Making your own methods for Instances

Hello, I’m Mega and this is my very first community tutorial!

So I have seen many people who want their custom methods on Instances but are not sure how they would make it work efficiently being global.
Metatables are great and we can utilize them here.

Lets try to make a system in which you can use services along with your custom functions and properties!

Step 1
Firstly lets start by making a service handler that would act as the middleman.
You could have it as a module script in ServerScriptService.

image
Now we can add some code that will let us retrieve Roblox Services, with or without our custom methods.

local module = {}

function module:GetService(serviceName)
    if script:FindFirstChild(serviceName) then
        return require(script[serviceName])
    end
    return game:GetService(serviceName)
end

return module

What this does is, returns the Custom Service module if it exists being a child of the script, if not it would return the roblox service.

Step 2
Time to make some custom methods for a service!
Lets make one for the Players service.

Start by making a module script inside our Services module. We will store all our custom methods in this module, make sure its called exactly the name of the service as that’s what our main script expects!
image
Now we’ll add some custom properties first.
If we wanted to store a value that could be accessed by all ServerScripts,
we could make ValueBases and update them.

However, we will try doing that by making a property that would let us efficiently retrieve it in any ServerScript.

Lets add it in our Players module script.

local module = {
	A = "This is a custom property!"
}


return setmetatable(module, {__index = game:GetService("Players")}) 
Detailed Explanation

As you may know, ModuleScripts are basically Tables. The way you would make a dictionary using the Key/Value pairs. We did the same thing here.
A would be the key or the name of the property, the value which is "This is a custom property!" could be called as the property’s default value, and don’t worry we can change it later!

Now, at the part where we return, you might be very confused.
Lets break it down,

You can set a so called metatable to an existing table, it basically allows us to extend its functionalities. setmetatable() is a function that takes 2 parameters, first would be the table we want to add it to, second would be the metatable itself with predefined events (You could call them that). These events are known as meta-methods.
The __index is one of them which fires whenever the main table gets indexed with something that is non-existant or nil. It has been set to the Roblox Service of Players so if we try to call a method on the module that doesnt exist, it would redirect it to the Players Service.

Now lets try this!
Insert a script in server script service

--I put this server script in Server Script Service.
local Services = require(script.Parent.Services)
local Players = Services:GetService("Players") --we actually retrieved the module straight as we had called `require()` at the time of returning!
						
print(Players.Name) --Players (As we didnt define `.Name` in our module, it took that from the actual players service)
print(Players.A) --Yes it will print our value that we stored								

Lets improve it further by making our own functions or methods

-- Our Players module					
local module = {
	A = "This is a custom property!";					
}
						
function module:KickAllPlayers() 
	for _, player in ipairs(game:GetService("Players"):GetPlayers()) do
		player:Kick()
	end
end						

return setmetatable(module, {__index = game:GetService("Players")}) 			

And yes, you would basically call it like any other function in a module script

--Our server script
local Services = require(script.Parent.Services)
local Players = Services:GetService("Players") --we actually retrieved the module straight as we had called `require()` at the time of returning!
						
print(Players.Name) --Players (As we didnt define `.Name` in our module, it took that from the actual players service)
print(Players.A) --Yes it will print our value that we stored	
Players.PlayerAdded:Connect(function(plr)
	Players:KickAllPlayers()
end)							

Other things to note

  • The values won’t be accessible by the client in our current setup, you could add it to replicated storage but that wont replicate the values
  • This can be done for any service, infact, for any Instance. Services have been used as an example.

Links

https://developer.roblox.com/en-us/api-reference/class/ModuleScript

I hope you learned something from this tutorial, I tried my best to make it as beginner friendly as possible,
Please let me know if you have any concerns or suggestion,
Thank you for reading!

55 Likes

FYI: This can be done for any Instance, not just services. Additionally, if you wanted to make them shareable to clients just put it in a place LocalScripts can reach (ie. ReplicatedStorage).

Eventually, Attributes will be released that’ll supersede using this.

3 Likes

Yeah, I’m fully aware of this, I used services as they make a useful example.
Putting it in ReplicatedStorage would still work, but both the server and client have different copies of the module scripts, so the value changes won’t replicate.

This really helped me with a project of mine, thanks!

1 Like

Great tutorial. My framework has something similar. Objects have their own function inserted into them, self:Wrap(Instance). They can then access/set properties of that wrapped instance just through self, and also call functions!

local MyObject = {};

function MyObject:MakeTransparent()
    self.Transparency = 1;
end

function MyObject:Created(Instance)
    self:Wrap(Instance);
    self.Parent = workspace;
end

return MyObject;

-- another module
local mynewobject = MyObject.new(workspace.Part);
mynewobject:MakeTransparent();
mynewobject:Destroy();

(^ assuming the Instance is a base part)

2 Likes

I personally like your framework aswell.

2 Likes
Code

– Script

local Services = require(script.ModuleScript)
local Players = Services:GetService("Players")
						
print(Players.Name)
print(Players.A)

for _, v in ipairs(Players:GetChildren()) do
	print(v)
end

Players.PlayerAdded:Connect(function(plr)
	Players:KickAllPlayers()
end)

– Service Manager

local module = {}

function module:GetService(serviceName)
    if script:FindFirstChild(serviceName) then
        return require(script[serviceName])
    end
    return game:GetService(serviceName)
end

return module

– Player Service

-- Our Players module					
local module = {
	A = "This is a custom property!";					
}
						
function module:KickAllPlayers() 
	for _, player in ipairs(game:GetService("Players"):GetPlayers()) do
--		player:Kick()
		print(player)
	end
end						

return setmetatable(module, {__index = game:GetService("Players")})

This doesn’t work if you try to call methods from the Instance it will error

for _, v in ipairs(Players:GetChildren()) do
	print(v)
end

Expected ‘:’ not ‘.’ calling member function GetChildren

I saw this flaw a few days ago but didn’t get the chance to prove it until just now


How do you address this issue? @Voidage

1 Like
3 Likes

Apologies for not testing the script properly :neutral_face:, thanking you for pointing out the flaw and @Voidage for the solution. :smile:

1 Like

Check out the Instance Wrapper section

Code
--| Script

local Services = require(script.InstanceWrapper)
local PlayerS = Services:GetService("Players")
						
print(PlayerS.Name)
print(PlayerS.A)
	
PlayerS.PlayerAdded:Connect(function(plr)
	PlayerS:KickAllPlayers()
	for _, v in ipairs(PlayerS:GetChildren()) do
		print(v)
	end
	print(#PlayerS:GetChildren())
end)

--| Instance Wrapper

local InstanceWrapper = {}

function InstanceWrapper:GetService(name)
	local module = require(script:FindFirstChild(name))
	local service = game:GetService(name)
    if module then
        return setmetatable(module,
		{
		__index = function(module_,index)
			local typof_ = typeof(service[index])
			if typof_ ~= "function" then
				return service[index]
			elseif typof_ == "function" then
				module_[index] = function()
					return service[index](service)
				end
				return module_[index]
			end
		end
		})
    end
    return service
end

return InstanceWrapper

--| Players

local Players = {
	A = "This is a custom property!";					
}
						
function Players:KickAllPlayers() 
	for _, player in ipairs(game:GetService("Players"):GetPlayers()) do
		print(player)
	end
end

return Players

Is this how you did it? @Voidage

2 Likes

It’s just a part of my framework, but yes that is part of how I handle wrapping objects. I think I used pcalls somewhere, to make sure an instance actually has a property or method.

It still wouldn’t work for Functions/Methods that accepts Parameters;

  • FindFirstChild(name)

I haven’t thought of that, but what I am thinking is just having those functions defined in the index function and when the index == “FindFirstChild” or any other similar function, then do what is appropriate

edit: wouldn’t work, args don’t pass.
so… what I could do is just make them custom functions of Objects. I don’t have to implement them for every single object I write, it will be inserted by the framework.

1 Like
Service Wrapper
--| Service Wrapper

local ServiceWrapper= {}

function ServiceWrapper:GetService(name)
	local module = require(script:FindFirstChild(name))
	local service = game:GetService(name)
    if module then
        return setmetatable(module,
		{
		__index = function(module_,index)
			local typof_ = typeof(service[index])
			if typof_ ~= "function" then
				return service[index]
			elseif typof_ == "function" then
				module_[index] = function(_,...)
					return service[index](service,...)
				end
				return module_[index]
			end
		end
		})
    end
    return service
end

return ServiceWrapper
  • I made it accept Arguments
  • Added cache to Function/Methods calls

I feel like this could be written a lot better, if you figured it out please proceed to do so

  • Doesn’t catch errors
    • Doesn’t error when index a nil value
    • Doesn’t have any type checks

Script.rbxm (1.7 KB)

6 Likes

Yeah that’s probably a better solution. Didn’t think of just passing … through the returned function

1 Like

This works really well, but i noticed one issue which is very critical for me, it errors when you try parenting instances to the wrapped instances.
image
How would we go on about fixing this?

1 Like

Sorry for bumping this post, but can we at least change already existing methods for instances?

I don’t think so. Because it’s pre-written methods, so you can’t. It’s just you can make your own.

1 Like

Thank you so much for this tutorial, I have been wanting to do something like this but didn’t know how.