Jump to content

Questions about custom OOP variables


Recommended Posts

Hello community!

A few days ago I jumped into learning OOP scripting and started creating a new vehicle system with it. I stumbled into a few problems when I finally created a basic vehicle creator script. I added fuel first of all, but when I wanted to create a gas station script for it (this would be in a different Lua file because of readability and organization), I couldn't think of how to get the fuel variable of the vehicle.

This problem got me thinking if I made the current system correctly at all?

Regarding to this problem I got one more question. I don't know how I should return the OOP variable on client side from server side, so I can show the fuel for example with dx functions, without setting different element datas for everything (because I would like to add a lot more features to the system, not just fuel) Is there a way to simply save variables or get them later somehow? Or is this what getmetatable function is used for?

Here is my current code, which is working how I wanted by the way, there is no problem right now, but I would like to make it as optimised, as simple as possible, and change it correctly regarding to my questions. Is there anything I did wrong or should be done otherwise and could be more efficient?

Server:

local db = exports["sa_db"]:getConnection()

local VEHICLE_FUEL_CONSUMPTION = 0.006

vehicleCreator = {totalSavedVehicles = 0, totalTemporaryVehicles = 0}
vehicleCreator.__index = vehicleCreator

function vehicleCreator:newVehicle(model, position, save)
	if not tonumber(model) then return outputDebugString("[vehiclecreator] 'model' parameter is required when creating a vehicle", 2) end
	if not position then return outputDebugString("[vehiclecreator] 'position' parameter is required when creating a vehicle", 2) end
	
	local newPosition = {x = position.x, y = position.y}
	local vehicleData = {
		element = Vehicle(model, position),
		fuel = 100,
		dirtLevel = 0,
	}
	setmetatable(vehicleData, self)
	
	local consumeFuel = function()
		local vehicle = vehicleData.element
		local distance = getDistanceBetweenPoints2D(vehicle.position.x, vehicle.position.y, newPosition.x, newPosition.y)
	
		local engineConsumption = 0
		if vehicle:getEngineState() then
			engineConsumption = 0.7
		end
		
		if vehicleData.fuel >= 1 then
			vehicleData.fuel = vehicleData.fuel - (VEHICLE_FUEL_CONSUMPTION*(distance+engineConsumption))
			newPosition.x, newPosition.y = vehicle.position.x, vehicle.position.y
			print(vehicleData.fuel)
		end
		
		if vehicleData.fuel < 1 then
			vehicleData.fuel = 0
			vehicle:setEngineState(false)
		end
	end
	
	addEventHandler("onVehicleEnter", vehicleData.element, function(_, seat)
		if seat == 0 then
			local fuelTimer = setTimer(function(vehicle)
				consumeFuel()
				vehicle:setData("vehicle.datatable", vehicleData)
			end, 1000, 0, source)
			addEventHandler("onVehicleExit", source, function(_, seat)
				if seat == 0 then
					if isTimer(fuelTimer) then
						killTimer(fuelTimer)
						fuelTimer = nil
					end
				end
			end)
		end
	end)
	
	if save then
		self.totalSavedVehicles = self.totalSavedVehicles + 1
		vehicleData.id = self.totalSavedVehicles
		
		dbExec(db, "INSERT INTO vehicles (id, model, position, rotation) VALUES (?, ?, ?, ?)",vehicleData.id,model,toJSON({x = position.x, y = position.y, z = position.z, int = 0, dim = 0}), toJSON({rx = 0, ry = 0, rz = 0}))
	else
		self.totalTemporaryVehicles = self.totalTemporaryVehicles + 1
		vehicleData.id = -self.totalTemporaryVehicles
	end
	
	vehicleData.element:setData("vehicle.datatable", vehicleData)
	
	return vehicleData
end

function vehicleCreator:deleteVehicle()
	self.element:destroy()
	if self.id > 0 then
		dbExec(db,"DELETE FROM vehicles WHERE id", self.id)
	end
	self = nil
end

function vehicleCreator:loadSavedVehicles()
	local query = dbQuery(db, "SELECT * FROM vehicles")
	local results = dbPoll(query, -1)
	
	self.totalSavedVehicles = 0
	for _, data in pairs(results) do
		local position = fromJSON(data.position)
		local vehicle = vehicleCreator:newVehicle(data.model, Vector3(position.x, position.y, position.z))
		
		self.totalSavedVehicles = self.totalSavedVehicles + 1
		vehicle.id = self.totalSavedVehicles
		print("id: " .. vehicle.id ..  " | fuel: " .. vehicle.fuel .. " | dirt: "  .. vehicle.dirtLevel)
	end
	dbFree(query)
end

addEventHandler("onResourceStart", resourceRoot, function()
	vehicleCreator:loadSavedVehicles()
end)


local testVehicle = vehicleCreator:newVehicle(500, Vector3(475.95861816406,-1841.2951660156,4.5377359390259)) -- creates vehicle at santa maria beach

Client:

local sx, sy = guiGetScreenSize()

function fueldisplay()
	local vehicle = localPlayer:getOccupiedVehicle()
	if vehicle then
		local vehicleData = getElementData(vehicle, "vehicle.datatable")
		dxDrawRectangle(sx/2-50, sy/3-25, 100, 50, tocolor(0, 0, 0, 150))
		dxDrawRectangle(sx/2-50, sy/3-25, (100*vehicleData.fuel)/100, 50, tocolor(100, 200, 100, 200))
	end
end
addEventHandler("onClientRender", root, fueldisplay)

I'm still learning OOP and there are some things I just couldn't understand how they work yet.

Thank you for your response in advance!

Link to comment

I have one more question, can I get a custom variable like self.fuel of a vehicle [element]? So let’s say I want to display the fuel on client, so I just have to write something like this

local vehicle = localPlayer:getOccupiedVehicle()

dxDrawText(vehicle.fuel ... —bla blaa

If it is, how can I achieve it? I really appreciate and accept every suggestions you think I might need to know about OOP and optimized easy to read code!???

Link to comment
  • Moderators
On 13/04/2020 at 08:58, Dzsozi (h03) said:

This problem got me thinking if I made the current system correctly at all?

 

 

 

	local consumeFuel = function()
		local vehicle = vehicleData.element
		local distance = getDistanceBetweenPoints2D(vehicle.position.x, vehicle.position.y, newPosition.x, newPosition.y)
	
		local engineConsumption = 0
		if vehicle:getEngineState() then
			engineConsumption = 0.7
		end
		
		if vehicleData.fuel >= 1 then
			vehicleData.fuel = vehicleData.fuel - (VEHICLE_FUEL_CONSUMPTION*(distance+engineConsumption))
			newPosition.x, newPosition.y = vehicle.position.x, vehicle.position.y
			print(vehicleData.fuel)
		end
		
		if vehicleData.fuel < 1 then
			vehicleData.fuel = 0
			vehicle:setEngineState(false)
		end
	end

You do not have to create another function, you can just use 1 function instead. Every new function you create within a block, will keep the block alive.

 

 


 

 

	addEventHandler("onVehicleEnter", vehicleData.element, function(_, seat)
		if seat == 0 then
			local fuelTimer = setTimer(function(vehicle)
				consumeFuel()
				vehicle:setData("vehicle.datatable", vehicleData)
			end, 1000, 0, source)
			addEventHandler("onVehicleExit", source, function(_, seat)
				if seat == 0 then
					if isTimer(fuelTimer) then
						killTimer(fuelTimer)
						fuelTimer = nil
					end
				end
			end)
		end
	end)

This code can cause a memory leak, since the code is merged with addEventHandler's that leave the Lua environment. Also here make use of 2 already existing function.

 

 


 

On 13/04/2020 at 08:58, Dzsozi (h03) said:

I don't know how I should return the OOP variable on client side from server side

If I want to have OOP tables (not meta table) shared between client/server. I have to split up the methods and re-attach them when they are available on the other side.

> [1 file] Client only methods

> [1 file] Server only methods

> [1 file] Shared methods

 

I do not know the best practice for this, but re-attaching them seems to work fine.

 

 


 

1 hour ago, Dzsozi (h03) said:

I have one more question, can I get a custom variable like self.fuel of a vehicle [element]? So let’s say I want to display the fuel on client, so I just have to write something like this

 

Maybe you can use rawset: (not sure if that is allowed, I do not use MTA OOP, only regular)

http://www.Lua.org/manual/5.1/manual.html#pdf-rawset

 

And if you can't edit those classes, you can just put another meta layer on top of it, to make it work.

 


 

I am not a pro,so I am pretty sure there are smarter people around here to give you the best answers.

 

Edited by IIYAMA
  • Like 1
Link to comment
9 hours ago, IIYAMA said:

You do not have to create another function, you can just use 1 function instead. Every new function you create within a block, will keep the block alive.

So then I don't understand how should I implement custom functions in my scripts, for example setFuel([element] vehicle, [int] value) and such things. I thought I must specify them inside the first table where I create and specify every default variable. Or you mean exactly like that, to create the above mentioned function then use a custom function outside of the vehicleCreator class where I use the fuel consumption method to update the fuel variable of the vehicle? Sorry, it isn't clear for me yet.

Because after that in a different gas station script, how do I use the self.fuel variable on a vehicle element? I found a script on the wiki that uses rawset for a backpack example:
https://wiki.multitheftauto.com/wiki/OOP_in_Lua (bottom of the page)
but it doesn't specify any real elements like vehicles, objects, peds etc.

9 hours ago, IIYAMA said:

If I want to have OOP tables (not meta table) shared between client/server. I have to split up the methods and re-attach them when they are available on the other side.

It is also not really clear to me what do you mean by re-attaching them. ? Should I make the base of the vehicleCreator class in a shared Lua file with variables I would like to share, I mean something like this:

-- stated as "shared" in meta

vehicleCreator = {
    lights = 0,
    engine = 0,
	fuel = 0,
}

-- stated as "server" in meta

function vehicleCreator:newVehicle(...)
	self.element = createVehicle(...)
  	self.fuel = 100
  	--etc etc
end

-- stated as "client" in meta

function displayFuel()
  	local vehicle = localPlayer:getOccupiedVehicle() -- or i guess i can use localPlayer.vehicle as well
  	if vehicle then
		dxDrawText(vehicle.fuel 200, 200, 0, 0)
    end
end
addEventHandler("onClientRender", root, displayFuel)

But while I was writing this, in my head other problems came up because of security and basic functioning.

I would like to make a simple and efficient system where I can use elements in any server side script to modify a once given variable and easily return it on client side for displaying when I need to (for example in this case let's just talk about the fuel, but later on I would like to expand variables to create a complex vehicle system with simple readable code ??).

Link to comment

I've watched every single MTA scripting tutorial from Daniel Lett, big shout outs to him btw, I learned some new things from their videos even though I am into scripting for a while now. My DB system is kinda similar to his from the videos because I want to remake my stuff in OOP, so basically kinda starting from stratch, I have an idea of the things I would like to do, I just don't know how to do some of them now that I experience with this OOP method.

And I think it is more efficient in some ways, mainly just because you write less text in codes, means less data to convert into zeros and ones. I don't know this advanced stuff that much, but so far this is what I heard and learned in videos I watched about OOP. I even watched Roblox OOP tutorials, but all I found was the basic setting up of tables and changing variables. But from this there comes no answer to my question about targeting a vehicle element and returning the (fuel) variable. ??

However I might just rewatch these videos in case I missed something.

Link to comment
  • Moderators

 

5 hours ago, Dzsozi (h03) said:

I would like to make a simple and efficient system where I can use elements in any server side script to modify a once given variable and easily return it on client side for displaying when I need to (for example in this case let's just talk about the fuel, but later on I would like to expand variables to create a complex vehicle system with simple readable code ??).

That is not so simple. Especially if you set a value from both sides at the same time. If there is a 'large' connection delay between the client and server.

Client says the vehicle has 70 fuel. When that message is still underway, serverside says the vehicle has been filled up to 100 fuel.

1. Client set 70

2. Server sets 100

3. Client sets 100

4. Server sets 70

 

Now it is desynchronized, so the first step is to set the rules. For example: The server is only allowed to set the fuel value.

 

You also need to make an collection of all vehicles, else you can't synchronize.

vehicleCreator = {totalSavedVehicles = 0, totalTemporaryVehicles = 0, vehicles = {list={}, register={}}}

 

The list is optional, it can be used to increase processing time for loops, since it is an array.

if not self.vehicles.register[vehicle] then
	self.vehicles.list[#self.vehicles.list + 1] = vehicleData

	self.vehicles.register[vehicle] = vehicleData
else
	return false
end

You can also turn these operations in to functions, but this is just basics.

 

 


 

5 hours ago, Dzsozi (h03) said:

So then I don't understand how should I implement custom functions in my scripts, for example setFuel([element] vehicle, [int] value) and such things. I thought I must specify them inside the first table where I create and specify every default variable. Or you mean exactly like that, to create the above mentioned function then use a custom function outside of the vehicleCreator class where I use the fuel consumption method to update the fuel variable of the vehicle? Sorry, it isn't clear for me yet.

If you have a register, you have already have access to those data. The vehicle is the key of course.

local data = vehicleCreator.vehicles.register[source]
if data then -- magic
  
end

-- or

function vehicleCreator:getVehicleData (vehicle)
	return self.vehicles.register[vehicle]
end

-- or

vehicleCreator.getVehicleData = function (self, vehicle)
	return self.vehicles.register[vehicle]
end

 


 

Did you know that the JS libraries jquery and D3 are working very similar to that? They just put their own layer on top of it.

 

jquery

var element = $( "#id" );

 

d3

var element = d3.select("#id")

 

Lua ???

local vehicleData = vehicleCreator:getVehicleData(vehicle)

 

I know you want to work cleanly but you have to keep in mind that in order to do that, you have to start wrapping a lot of things.
> Even if you did save these information into the class, you will lose that as soon as the element is destroyed. Which is another thing to keep in mind.

 

 

 

 

 

 

 

 

Link to comment

Alright, I am getting closer to what I need, but I think I am not doing something correctly, also I am still stuck at the "variable-data-transferring" between client and server.

I rewrote the script based on your comments and Daniel Lett's videos,  now it looks a little bit different.

-- i got this function from Daniel's video
function Class(tbl)
	-- set a metatable on the passed tbl parameter
	-- __call property
	-- create a new table
	-- on that new table, set __index to our class
	-- call a constructor method on the class
	-- return our new table
	
	setmetatable(tbl, {
		__call = function(cls, ...)
			local self = {}
			
			setmetatable(self, {
				__index = cls
			})
			
			self:constructor(...)
		end
	})
	
	return tbl
end
------

local db = exports["sa_db"]:getConnection()

local numberToBoolean = {
	[0] = false,
	[1] = true,
}

local switchToOverrideLights = {
	[0] = 1,
	[1] = 2,
}
--local VEHICLE_FUEL_CONSUMPTION = 0.006   -- not using it yet
--local VEHICLE_START_KEY = "J"

local totalSavedVehicles = 0
local totalTemporaryVehicles = 0

vehicleCreator = Class({
	vehicles = {},
	
	newVehicle = function(self, model, position, rotation, save)
		if not tonumber(model) then return outputDebugString("[vehiclecreator] 'model' parameter is required when creating a vehicle", 2) end
		if not position then return outputDebugString("[vehiclecreator] 'position' parameter is required when creating a vehicle", 2) end
		rotation = rotation or Vector3(0,0,0)
		
		local vehicle = Vehicle(model, position)
		if save then 
			totalSavedVehicles = totalSavedVehicles + 1
			dbExec(db, "INSERT INTO vehicles (id, model, position, rotation) VALUES (?, ?, ?, ?)",totalSavedVehicles,model,toJSON({x = position.x, y = position.y, z = position.z, int = 0, dim = 0}), toJSON({rx = rotation.x, ry = rotation.y, rz = rotation.z}))
		else
			totalTemporaryVehicles = totalTemporaryVehicles - 1
		end
		
		local vehicleData = {
			id = save and totalSavedVehicles or totalTemporaryVehicles,
			fuel = 100,
		}
		self.vehicles[vehicle] = vehicleData
		
		self:toggleEngine(vehicle, 0)
		self:toggleLights(vehicle, 0)
		
		return self.vehicles[vehicle]
	end;
	
	getVehicleData = function(self, vehicle)
		return self.vehicles[vehicle]
	end;
	
	deleteVehicle = function(self, vehicle)
		if self.vehicles[vehicle] then
			if self.vehicles[vehicle].id > 0 then
				dbExec(db,"DELETE FROM vehicles WHERE id", self.vehicles[vehicle].id)
			end
			self.vehicles[vehicle] = nil
			vehicle:destroy()
			return true
		end
		return false
	end;
	
	toggleEngine = function(self, vehicle, state)
		if self.vehicles[vehicle] then
			self.vehicles[vehicle].engine = state
			return vehicle:setEngineState(numberToBoolean[state])
		end
	end;
	
	toggleLights = function(self, vehicle, state)
		if self.vehicles[vehicle] then
			self.vehicles[vehicle].lights = state
			return vehicle:setOverrideLights(switchToOverrideLights[state])
		end
	end;
})


-- testing

local testveh = vehicleCreator:newVehicle(475, Vector3(-296.25921630859,1751.9106445313,42.6875), nil)
print(#vehicleCreator.vehicles) -- why do i always get 0?

addEventHandler("onVehicleStartEnter", root, function()
	--local data = vehicleCreator:getVehicleData(source)
	--if data.id < 0 then
		--vehicleCreator:deleteVehicle(source)
	--end
	vehicleCreator:toggleEngine(source, 1)
	vehicleCreator:toggleLights(source, 1)
end)

So my questions are

why does #vehicleCreator.vehicles return 0 even though there is clearly a created vehicle (I tried it with multiple vehicles as well)?
how can I send the fuel variable from server to client?
how can I get the fuel variable of a vehicle in a different resource? (I guess setElementData would be the easiest but I would like to avoid it as much as possible)
Do I have to use exported functions and triggers from server to client to get the variables? Because that would also cause desync for sure.

Lets say I have vehicleCreator and vehicleScripter resources. In vehicleScripter resource I store the scripts about gas stations, fueling up the vehicles, etc, and in vehicleCreator I create them for shops and admin purposes, etc. How can reach a variable or method stated in vehicleCreator from vehicleScripter, for example the toggleEngine.

Regarding the element data, should I make a custom data system which uses only one element data (let's say vehicle.data) to store a table of information (let's say {fuel=100, engine=1, ligths=1})? Is it more efficient or basically I am at the same position because of the length of the tables? I hope my question makes sense and you can answer me. ?

Edited by Dzsozi (h03)
Link to comment
  • Moderators
8 minutes ago, Dzsozi (h03) said:

why does #vehicleCreator.vehicles return 0 even though there is clearly a created vehicle (I tried it with multiple vehicles as well)?

Because the table is not an array format. That is why I split up the formats in to register and list.

if not self.vehicles.register[vehicle] then
	self.vehicles.list[#self.vehicles.list + 1] = vehicleData

	self.vehicles.register[vehicle] = vehicleData
else
	return false
end

 

#self.vehicles.list -- item count

#self.vehicles.register -- always 0

 

 

In your case you can do 4 things.

Recount

local count = 0
for _,_ in pairs(vehicleCreator.vehicles) do
  count = count + 1
end

 

Manually keep track of the count:


self.vehicleCount = self.vehicleCount + 1 -- when you add a vehicle

self.vehicleCount = self.vehicleCount - 1 -- when you remove a vehicle

 

Or use an array format. --- at the cost of more search work with loops

 

Or use 2 formats: array and custom indexes (vehicle = key) -- at the cost of more clean work

 


 

15 minutes ago, Dzsozi (h03) said:

how can I send the fuel variable from server to client?

I can answer that with an easy answer, but I do not think that would match your case, since that would kill some people their network. It is very important to write down the synchronization rules and pay a lot of attention to data reduction. Give it a 20 min brainstorm. Do you have to receive data when you are inside of a vehicle? Or when you are close by? Do you send the same value X when the user already know that the value is X?

 

-- easy answer
triggerClientEvent(players, "sync-vehicle-fuel", resourceRoot,  {
vehicleData1, vehicleData2, vehicleData3
})

 

addEvent("sync-vehicle-fuel", true)
addEventHandler("sync-vehicle-fuel", resourceRoot, 
function (data) 

end, false)

 

 

25 minutes ago, Dzsozi (h03) said:

Do I have to use exported functions and triggers from server to client to get the variables?

Exports do not work across clientside and serverside.

 

27 minutes ago, Dzsozi (h03) said:

should I make a custom data system which uses only one element data (let's say vehicle.data) to store a table of information (let's say {fuel=100, engine=1, ligths=1})?

Could work, but you can't reduce data. Element data maybe be smaller than triggerServerEvents, but you no control.

But I do recommend to test both, so that you can get an idea, which one works better for you.

 


 

To make the fuel system feel smoother, instead of only regular updates, you can also consider adding something that predicts what the fuel will be on clientside.

So for example the server sends every 5 second an update.

The updates from serverside: 100 > 95 > 90

While on clientside you make an future prediction and do: 100 > 99 > 98 > 97 > 96 > 95 > 94 > 93 > 92> 91 > 90

 

 

 

 

 

 

Link to comment
20 minutes ago, IIYAMA said:
1 hour ago, Dzsozi (h03) said:

Do I have to use exported functions and triggers from server to client to get the variables?

Exports do not work across clientside and serverside.

I meant to write "... to get the variables from another resource". :D The question is how can I return something like vehicle.fuel in a different resource when hitting a marker for example. But I guess I will just stick to element data then with a custom data system, at the moment I really don't know any other solutions, triggering back and forth sounds a lot of inefficient and unnecessary work.

But how can I call a method from a different resource like I mentioned above? In vehicleScripter resource how do I call the vehicleCreator:toggleEngine function, or is there any way to just use vehicle:toggleEngine? Do I make an exported function to get the vehicleCreator table? Or what is the solution for this?

Edited by Dzsozi (h03)
Link to comment
  • Moderators
25 minutes ago, Dzsozi (h03) said:

triggering back and forth sounds a lot of inefficient and unnecessary work.

Maybe, but you do not really need a lot of work to make that to work.

 

addCommandHandler("callclient", function (player) 
	
	-- An addCommandHandler is needed, because the client hasn't loaded it's scripts yet.
	callClient(player, "hello", 
	function (value) 
		iprint(value)
	end)
	
end, false, false)

 

function hello ()
	return exports. -- ...
end

 

 

 

25 minutes ago, Dzsozi (h03) said:

But how can I call a method from a different resource like I mentioned above? In vehicleScripter resource how do I call the vehicleCreator:toggleEngine function, or is there any way to just use vehicle:toggleEngine? Do I make an exported function to get the vehicleCreator table? Or what is the solution for this?

That is not possible, as the table reference can't be transferred to the other side. When you export tables from 1 resource to another, it deep copies the table. And functions can't be transferred at all.

If you really want that. You need to transfer the method as a string.

 

local methods = {
	{
	key = "test1",
    value = [[
		function (self, something)
			return somthing * 2
		end
    ]]
	},
	{
	key = "test2",
    value = [[
		function (self, something)
			return somthing * 2
		end
    ]]
	}
}

 

 

Edited by IIYAMA
Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...