Metamethod for when instance property gets set to table?

Hello :slight_smile: ,

The title might sound confusing but what I’m trying to do is quite simple. I have made a wrapper for a custom Vector3 for fun. I probably wont use it for anything unless I can make really useful stuff with it. But there is one slight issue that I have and that has to do with setting a Roblox instance’s position or size to the custom Vector3 type. (Or any property that uses Vector3) I’ve been using metatables and metamethods for the basic functions and now I just need a way to do that.

There is a simple way of doing it but I’d like to instead only give the custom Vector3 (It’s just a table connected to a metatable with some properties)

The simple way:

local VectorModule = require(script.VectorModule)
local part = workspace.part
local Vector = VectorModule.new(0, 5, 3) -- I make a Vector3
part.Position = Vector3.new(Vector.X, Vector.Y, Vector.Z)

What I want to achieve:

local VectorModule = require(script.VectorModule)
local part = workspace.part
local Vector = VectorModule.new(0, 5, 3)
part.Position = Vector

But obviously you can’t set a part’s position with a table. I was wondering if there was a metamethod to be able to convert this custom Vector table into an actual Vector3 behind the scenes but I couldn’t find this kind of metamethod anywhere on this website: Metatables
Is there no way of doing this? I’m also new to using metatables so feedback for the wrapper would be appreciated!

My custom vector wrapper:

local module = {}
-- Axis X = i, Axis Y = j, Axis Z = k

function module.calculateMagnitude(Vector) : number
	return math.abs(math.sqrt(Vector.X ^ 2 + Vector.Y ^ 2 + Vector.Z ^ 2))
end

function module.calculateUnitVector(Vector)
	return Vector / Vector.Magnitude
end

function module.calculateVector(Vector) : nil
	Vector.Magnitude = module.calculateMagnitude(Vector)
	if rawget(Vector, "Unit") ~= nil then
		local Estimate = module.calculateUnitVector(Vector)
		if not (Estimate == Vector.Unit) then
			Vector.Unit = Estimate
		end
	end
end

function module.new(X : number, Y : number, Z : number) 
	local Vector = {}
	setmetatable(Vector, module)
	Vector.X, Vector.Y, Vector.Z = X, Y, Z
	module.calculateVector(Vector)	
	return Vector
end

function module:setX(NewX : number)
	self.X = NewX
	module.calculateVector(self)
end

function module:setY(NewY : number)
	self.Y = NewY
	module.calculateVector(self)
end

function module:setZ(NewZ : number)
	self.Z = NewZ
	module.calculateVector(self)
end

function module:Dot(Vector) : number
	return self.X * Vector.X + self.Y * Vector.Y + self.Z * Vector.Z
end

function module:Cross(Vector)
	local DeterminantI = self.Y * Vector.Z - self.Z * Vector.Y
	local DeterminantJ = self.X * Vector.Z - self.Z * Vector.X
	local DeterminantK = self.X * Vector.Y - self.Y * Vector.X
	local X, Y, Z = DeterminantI, DeterminantJ == -0 and 0 or -DeterminantJ, DeterminantK
	local CrossVector = module.new(X, Y, Z)
	return CrossVector
end

function module.zero() return module.new(0, 0, 0) end 
function module.one() return module.new(1, 1, 1) end 
function module.xAxis() return module.new(1, 0, 0) end
function module.yAxis() return module.new(0, 1, 0) end
function module.zAxis() return module.new(0, 0, 1) end

--<< Stuff >>--
function module.__index(t, Value)
	if module[Value] == nil then
		if Value == "Unit" then
			t.Unit = module.calculateUnitVector(t)
			return t.Unit
		end
	else
		return module[Value]
	end
end

--<< Operator Functions >>--
function module.__add(t, Value)
	if type(Value) == "number" then
		return module.new(t.X + Value, t.Y + Value, t.Z + Value)
	elseif type(Value) == "table" or typeof(Value) == "Vector3" then
		return module.new(t.X + Value.X, t.Y + Value.Y, t.Z + Value.Z)
	end
	error("Vectors can only be added by numbers or other vectors!")
end

function module.__sub(t, Value)
	if type(Value) == "number" then
		return module.new(t.X - Value, t.Y - Value, t.Z - Value)
	elseif type(Value) == "table" or typeof(Value) == "Vector3" then
		return module.new(t.X - Value.X, t.Y - Value.Y, t.Z - Value.Z)
	end
	error("Vectors can only be subtracted by numbers or other vectors!")
end

function module.__mul(t, Value)
	if type(Value) == "number" then
		return module.new(t.X * Value, t.Y * Value, t.Z * Value)
	elseif type(Value) == "table" or typeof(Value) == "Vector3" then
		return module.new(t.X * Value.X, t.Y * Value.Y, t.Z * Value.Z)
	end
	error("Vectors can only be multiplied by numbers or other vectors!")
end

function module.__div(t, Value)
	if type(Value) == "number" then
		return module.new(t.X / Value, t.Y / Value, t.Z / Value)
	elseif type(Value) == "table" or typeof(Value) == "Vector3" then
		return module.new(t.X / Value.X, t.Y / Value.Y, t.Z / Value.Z)
	end
	error("Vectors can only be divided by numbers or other vectors!")
end

function module.__eq(t, Value, x)
	print(t, Value, x)
	if type(Value) == "table" then
		return (t.X == Value.X and t.Y == Value.Y and t.Z == Value.Z) and true or false
	else
		error(Value .. " is not the same datatype!")
	end
end

--<< Printing >>--
function module.__tostring(self)
	return table.concat({self.X, self.Y, self.Z}, ", ")
end

return module

Nope, there’s no fallback to gets the table implicitly casted to Vector3 in the way that you want as Roblox does not implement this.

You could explicit cast it by implementing :ToNativeVector3() method, then do something like this:

local VectorModule = require(script.VectorModule)
local part = workspace.part
local Vector = VectorModule.new(0, 5, 3) -- I make a Vector3
part.Position = Vector3:ToNativeVector3() 

Or you could create your own Instance wrapper that does the way you want to.

1 Like

You can’t define custom metamethods for Roblox’s built-in datatypes. What you could do is alter your constructor function slightly so that each object stores a copy of the corresponding Vector3 value.

function module.new(X : number, Y : number, Z : number) 
	local Vector = {}
	setmetatable(Vector, module)
	Vector.X, Vector.Y, Vector.Z = X, Y, Z
	Vector.V = Vector3.new(X, Y, Z)
	module.calculateVector(Vector)	
	return Vector
end

local VectorModule = require(script.VectorModule)
local part = workspace.part
local Vector = VectorModule.new(0, 5, 3) -- I make a Vector3
part.Position = Vector.V
1 Like

Both of these solutions work, but storing another normal Vector3 inside the custom one would be complicated and every time you set the custom one the actual Vector3 has to be changed too. Thanks for helping though :grin:!