Services are also constants. Would it be weird just capitalizing them too?
local USER_INPUT_SERVICE = game:GetService("UserInputService")
Services are also constants. Would it be weird just capitalizing them too?
local USER_INPUT_SERVICE = game:GetService("UserInputService")
Do yourself and future collaborators a favor, teach yourself to write it as
local UserInputService = game:GetService("UserInputService")
General form: local A = game:GetService("A")
Reserve the uppercase-underscore notation for static values that you expect someone might want to tweak or when it leads to better code semantics when they are named instead of anonymous values (i.e. magic values in your code).
Here’s an example of my class implementation btw:
HIERARCHY
WEAPON CLASS
local Weapon = {}
Weapon.__index=Weapon
function Weapon:Equip(modules,Main)
print(self.weptype.." equipped")
modules.clientModules.Gui:ShowWepInfo(Main)
modules.clientModules.Gui:UpdateWepInfo(self,Main)
end
function Weapon:Unequip(trackFile,Gui,Main)
print(self.weptype.. " unequipped!")
Gui:HideWepInfo(Main)
--stop tracks
for i, v in pairs(trackFile) do
v:Stop()
end
end
function Weapon:Sprint(trackFile,modules)
if trackFile.unSprint then
trackFile.unSprint:Stop()
end
trackFile["sprint"]=modules.clientModules.Animate.new("Fire")
trackFile["sprint"]:Play()
end
function Weapon:UnSprint(trackFile,modules)
if trackFile.sprint then
trackFile.sprint:Stop()
end
trackFile["unSprint"]=modules.clientModules.Animate.new("Fire")
trackFile["unSprint"]:Play()
end
function Weapon.new(weptype)
local wep = {
weptype=weptype,
}
setmetatable(wep,Weapon) --inherit all wep functions
return wep
end
return Weapon
FIREARM CLASS (subclass of superclass “Weapon”)
local Weapon = require(script.Parent.Parent)
local Firearm = {}
Firearm.__index=Firearm
setmetatable(Firearm,Weapon)
function Firearm:Fire(trackFile,player,modules,Main)
if self.mag>0 then
if self.fireType == "Semi" then
self.mag=self.mag-1
--print("mag - "..self.mag)
modules.clientModules.Gui:UpdateWepInfo(self,Main)
trackFile["fireTrack"]=modules.clientModules.Animate.new("Fire")
trackFile["fireTrack"]:Play()
end
else
self:Reload(modules.clientModules.Gui,Main)
end
end
function Firearm:Reload(Gui,Main)
if self.mag<self.max and self.pool > 0 then
local needed = self.max-self.mag
local toAdd
if self.pool-needed > 0 then
toAdd=needed
self.pool=self.pool-needed
else
toAdd=self.pool
self.pool=0
end
self.mag=self.mag+toAdd
--print("pool - "..self.pool)
--print("mag - "..self.mag)
Gui:UpdateWepInfo(self,Main)
end
end
function Firearm.new()
print("new")
local firearm = Weapon.new("Firearm")
firearm.pool=100
firearm.max=10
firearm.mag=10
firearm.fireType="Semi"
setmetatable(firearm,Firearm)
return firearm
end
return Firearm
Anything I should fix or “improper”?
Just a couple notes.
A quick look over the rest looks good, but I need to go again and will be back in a bit.
I also have modules under the client main script
Animate
local Animate = {}
Animate.__index=Animate
function Animate:Update(dt)
if self.playing then
self.t=math.min(self.max,self.t+dt)
print(self.t)
end
end
function Animate:Play()
self.playing=true
end
function Animate:Pause()
self.playing = false
end
function Animate:Stop()
self.t=self.max
end
function Animate.new(animName)
local animTrack = {
playing=false,
t=0,
max=1,
}
setmetatable(animTrack,Animate)
return animTrack
end
return Animate
Gui
local Gui = {}
--static functions
function Gui:ShowWepInfo(Main)
Main.WepInfo.Visible = true
end
function Gui:HideWepInfo(Main)
Main.WepInfo.Visible = false
end
function Gui:UpdateWepInfo(wep,Main)
if wep.weptype == "Firearm" then
Main.WepInfo.Pool.Text=wep.pool
Main.WepInfo.Mag.Text=wep.mag
else
Main.WepInfo.Pool.Text="-"
Main.WepInfo.Mag.Text="-"
end
end
return Gui
Should I change up this organization?
Some more notes about the Weapon and Firearm classes:
firearm.pool=100
should be firearm.pool = 100
. These files have mixed use, which is an abomination in the sight of all that is holy! A place that is optional to use is after { and before }. Some think it is easier to read, other are indifferent.weptype
turns into type
which the IDE then highlights as a special function, but look for other ways to name it. In this case, I believe className
would be more appropriate.weptype
if it was to be used should also be wepType
weptype
(now className
) should be defined by the child, not passed into the parent-- Class Weapon
--function Weapon.new()
--...
--end
-- nothing needs to be initialized in a weapon
function Weapon:init()
end
-- Class Firearm
local CYCLE = {
SEMI = 'Semi';
}
local Firearm = {
super = Weapon;
}
Firearm.__index = Firearm
setmetatable(Firearm, Firearm.super)
-- ...
function Firearm:init()
Firearm.super.init(self)
self.pool = 100
self.max = 10
self.mag = 10
self.cycle = CYCLE.SEMI
end
--function Firearm.new()
--...
--end
-- In a file of shared functions
local function new(class, ...)
local obj = {}
class.init(obj, ...)
return setmetatable(obj, class)
end
This prevents both the super and the sub class setting the object’s metatable.
fireType
to cycle
and made a constant table, much like an enumeration. In looking for a better name, wikipedia said this: “Firearms are also categorized by their functioning cycle or “action” which describes its loading, firing, and unloading cycle.” So I took the common word, cycle, to describe that functionality rather than generic fireType. local toAdd
if self.pool-needed > 0 then
toAdd=needed
self.pool=self.pool-needed
else
toAdd=self.pool
self.pool=0
end
self.mag=self.mag+toAdd
self.mag = self.mag + [something]
can be inlined and self.pool - needed > 0
is mathematically equivilent to self.pool > needed
which changes produce this: if self.pool > needed then
self.mag = self.mag + needed
self.pool = self.pool - needed
else
self.mag = self.mag + self.pool
self.pool = 0
end
or since 0 = self.pool - self.pool,
local delta = math.min(needed, self.pool)
self.mag = self.mag + delta
self.pool = self.pool - delta
As for project organization, I recommend putting most client modules in Replicated Storage, but the way you have it is fine. This was a recent topic on the forum: How do you optimize and organize your code?
Hmm. Looking over these posts, I see why good software engineers are paid so much… there really is a lot to it!
Another thing, why is it bad to have a var table?
This is how I deal with the 60 upvalues thing.
What happens if a connected module (my client is broken up into different modules) needs to change a value on the main client code? It would need a table wouldn’t it?
Also why do I need an init method?
Should calling the .new constructor with the necessary parameters to set the variables be enough?
Seems like having an init thingy makes it more complicated.
Using a var table shared in your code is like using the shared
or _g
table in your code. It tightly binds your functions together and tracing where a value was edited or all the places that can affect a variable can become difficult. In large projects, it becomes impossible. Not using shared state and following the principle of least privilege reduces debugging time and makes creating bugs more difficult.
Separating the initialization of a class from instantiation is required for clean inheritance. For example, when instantiating a child class, both the initialization of the parent and the child must be run, but only one instance is created. I changed the name from new to init to signify the difference.
hm, what do i do if i want to change something from the main code like a variable thats not a table (without a vars table) on a module script connected to that main code??
is it bad practice to have a module script act as an “extension” to your main script??
Information exchange is usually done using arguments and results, function input and output. The main script would create a wholly independent instance of a class, which may in turn create more instances. The main file will then use class methods to manipulate the instance it created, and the instance will return values which are results of that operation which the main script can use with conditional branching to decide what to do next.
Class, as much as possible, should be completely independent and each object should, again for the most part, contain its own state. This help isolate errors and bugs. Most of the effort of good program design is in determining the relationships between information. OOP most of the time can accurately capture the relationships efficiently and create highly compartmentalized programs, but not always. This is one reason why I like Data Oriented Programming rather than Object Oriented Programming. Your project structure will most likely fit into an OOP model, but you may have to do some rejiggering to get it there.
I’d note that Java programs (a language that tried and failed to be fully OOP) do not allow a main method outside of a class. They don’t allow any methods outside of classes. That is a functional language feature, not an OOP one. The more code and data residing in your main function rather than cleanly split out into classes and participating in relationships with other objects, the more you are trying to mash two methods of programming together and difficulty you will have.
ECS (entity-component-system) frameworks are designed to help large projects organize their code and reduce dependencies. When I used my ECS implementation in a project, the main file simply loaded all the components and systems into the framework, then created the first entity, applied the Main
assembly to that entity, and finally began the update loop (calling ECS.update()
after every wait()
). It was a very simple file. All of the program’s logic was contained inside of the framework.
By “extension” I think your implying tightly connected and sharing lots of information. Not all of the time. Sometimes with OOP, creating large classes is the only way handle complex relationships. When this happens, it is desirable to split the class’s implementation into multiple files. Is this optimal? No. Optimally all of your files will by 70 - 80 lines (about two screens worth) of code and contain simple logic. None of the files would have duplicate logic. Information flow through the program would be simple and predictable. But alas, the optimal program is ever elusive to most program designers.
One last thing about folder organization. I have a module with methods but they’re all static. There’s no object creation in them. What folder name would these type of modules go under? ‘Services’??
A collection of functions is called a library. Lua naturally has the string, table, math, and other libraries built in. A common custom library name is ‘util’, short for utility, and contains lots of commonly used functions.
On a side note, OOP classes have methods (in Lua, functions called with ‘:’) and methods are functions with a ‘this’ or ‘self’ bound onto it. Pure functional programs don’t have methods and pure OO programs don’t have functions.
So let’s say for my Gui Util, which is organized like this:
(tell me if there’s anything wrong with this btw)
Here’s the code:
--constants
local PLAYER = game.Players.LocalPlayer
local PLAYERGUI = PLAYER:WaitForChild("PlayerGui")
local MAIN = PLAYERGUI:WaitForChild("Main")
local Gui = {}
--static functions
function Gui:ShowWepInfo()
MAIN.WepInfo.Visible = true
end
function Gui:HideWepInfo()
MAIN.WepInfo.Visible = false
end
function Gui:UpdateWepInfo(wep)
if wep.type == "Firearm" then
MAIN.WepInfo.Pool.Text=wep.pool
MAIN.WepInfo.Mag.Text=wep.mag
else
MAIN.WepInfo.Pool.Text="-"
MAIN.WepInfo.Mag.Text="-"
end
end
return Gui
If I want to make it so you click Main.Button
would I write that function directly into the Gui module code?
OR would I write it in my main code and call a static method on Gui?
For example:
--main code
--constants
local PLAYER = game.Players.LocalPlayer
local PLAYERGUI = PLAYER:WaitForChild("PlayerGui")
local MAIN = PLAYERGUI:WaitForChild("Main")
local MODULES = ReplicatedStorage:WaitForChild("Modules")
local UTIL = MODULES:WaitForChild("Util")
local GUI = require(UTIL:WaitForChild("Gui"))
MAIN.Button.MouseButton1Click:connect(function()
GUI:SomeMethod()
end)
Which way is better practice and the proper way to do it??
You have an interesting problem on your hands. But first, some feedback.
He said there are two situation in which you should use uppercase variables:
Variables that commonly fit these definitions are usually being used as constants being passed to functions / instances outside your code. Many times these are ‘configurable’ values. Values used as enumerations and UDims are also commonly put at the top of the file. Under this new definition, MAIN
and PLAYER_GUI
do not meet the criteria for being capitalized. Sorry about that.
A couple small things. I still see assignments without spaces. Also I corrected this in my one of the copies of your code above hoping you would see it, but :connect
is now :Connect
at least in part to conform with standard Roblox capitalization standards. The old method is deprecated. I’d consider renaming Gui to be more specific like WeaponGui. If it really is a class dealing with everything to do with the gui, then it should be broken down a bit. I saw that you renamed weptype
.
Who owns the button connection code depends on who is interested in the button’s behavior. How much is the GUI class in charge of? If it is all GUI code, then it belongs in the GUI class. But then the question becomes, where to put it in the GUI class? I’d bet this is the actual question that made you post. The problem is that you’ve fallen into a common anti-pattern some programmers think is beneficial (but don’t believe them!) and are having troubles because of it. I see the enticement of making a singleton for a GUI that is only created once per a client, but it really should be a class with only one instance. OOP isn’t about being efficient (again, a reason I detest OOP) and naturally has overhead to obey its design decisions (basic getters and setters, single instance instead of static, and other things. But I digress.
Wikipedia says this about singletons in general
But I say it is even more so in OOP. Static methods and classes are also often considered anti-patterns (see Anti-Patterns and Worst Practices). This includes Utility classes. Consider this article instead:
OOP alternative to Utility Classes They are all features of functional programming and have snuck into OOP languages like Java to taint them.
Now, that being said, you can use static methods and classes. Some think singletons are a good pattern. Where will you put the connection for the button? I’m not sure, as code outside of classes doesn’t belong in a class file but then again main shouldn’t have its nose is every other modules’ business. If you were to make GUI a class instead of a singleton, it would neatly fit into the constructor, and the PlayerGui.Main would be passed in as a constructor argument and become a property. If you choose to make it a static class, I’d remove the ‘:’ because those functions are not using self. They are not methods. (Yes, I said functions do not exist in OOP, and I stand by that). I’d recommend going on a static class refactoring spree with the knowledge in the articles above to make it all OOP.
Or, you know, decide to join me in a campaign against OOP, the disgusting paradigm that for some reason is claimed to be natural to programmers and clean? We’ve clearly seen that functional programming is more natural and ingrained to every programmer that even the designers of THE OOP language let it sneak in! It doesn’t match the data and operations, thrashes CPU caches, destroys locality of reference, and leaves anti-patterns and code smells in any actual project’s wake? Its bad for the programmers and bad for the machines. There is a reason Java VMs are written in C and C++. Its because while they may offer OOP support, the still have some sanity left in them. Yes, it would be a glorious campaign. (I’m sorry, I had to let it out…)
Just to make it clear, you did say its good practice to keep things independent of each other? Especially my custom made classes?
Bc I did a lot of revisions to the code since the first post.
Yes, in all cases you want code to be compartmentalized, and independent. Classes in OOP and functions are meant to hide details from their creator/callers, not simply contain operations. They simplify their users’ lives. The principle of least privilege isn’t just to prevent nefarious users from taking advantage of information (like the creation of anti-patterns) but also to prevent simple bugs and ease the strain on a developer’s mind. They shouldn’t have to worry about the whole program when working on a little part of it. Most of us arn’t on top of our game 100% of the time.
You know, when I worked at Roblox, I think people dreaded having their code reviewed by me…
Ok, bc I changed my weapon class from this:
OLD
--services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--constants
local PLAYER = game.Players.LocalPlayer
local PLAYERGUI = PLAYER:WaitForChild("PlayerGui")
local MAIN = PLAYERGUI:WaitForChild("Main")
local MODULES = ReplicatedStorage:WaitForChild("Modules")
--services
local SERVICES = MODULES:WaitForChild("Services")
local GUI = require(SERVICES:WaitForChild("Gui"))
local Weapon = {}
Weapon.__index=Weapon
function Weapon:Equip()
print(self.type.." equipped")
GUI:ShowWepInfo(MAIN)
GUI:UpdateWepInfo(self,MAIN)
end
function Weapon:Unequip()
print(self.type.. " unequipped!")
GUI:HideWepInfo(MAIN)
--stop tracks
for i, v in pairs(self.trackFile) do
v:Stop()
end
end
function Weapon.new()
local wep = {equipped=false}
setmetatable(wep,Weapon) --inherit all wep functions
return wep
end
return Weapon
to this
NEW
local Weapon = {}
Weapon.__index=Weapon
function Weapon:Equip()
print(self.type.." equipped")
self.equipped = true
end
function Weapon:Unequip()
print(self.type.. " unequipped!")
self.equipped = false
end
function Weapon.new()
local wep = {equipped=false}
setmetatable(wep,Weapon) --inherit all wep functions
return wep
end
return Weapon
Is this what you mean when you say least privilege and independent? Am I doing it right?
Ah, I see. It does look good to me. You’re doing it right! But it does make me wonder, how are the GUI interactions and animations being controlled now? If you are still looking for a good way to do this and haven’t done so already, I’d recommend making a Player class. That class, which creates and maintains the PlayerGui, Character, and Backpack related classes should contain a unequip method since equipping deals with scripts from all three of those areas of responsibility. In OOP you yoink code up higher in the hierarchy to deal with access issues like this. Unfortunately this is also another reason I don’t like OOP as it creates heavy top-level classes.
I’m beginning to think you have a vendetta against spaces. You can’t just backspace everything you don’t like! lol
(Interesting, in the quote above, two periods show up as three…)
lol i didn’t do anything about the spaces yet but ill get to them eventually
GUI interactions and animations are being controlled by the main code now, for example:
--unequip current, if
if equipped then
weps[equipped]:Unequip()
--stop tracks
for i, v in pairs(tracks[PLAYER]) do
v:Stop()
end
end
--equip
equipped = inputObj.KeyCode.Value-48
weps[equipped]:Equip()
--update gui
GUI:ShowWepInfo(MAIN)
GUI:UpdateWepInfo(weps[equipped],MAIN)
Would my Gui module be considered a singleton. If so, whats the single object under that class? I’m still have trouble classifying what type of module it is. And I’m still unclear if it should be an extension of my main script for not.
Also, is it bad having a single main script to control everything?