Dzsozi (h03) Posted April 13, 2020 Share Posted April 13, 2020 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
Dzsozi (h03) Posted April 15, 2020 Author Share Posted April 15, 2020 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 IIYAMA Posted April 15, 2020 Moderators Share Posted April 15, 2020 (edited) 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 April 15, 2020 by IIYAMA 1 Link to comment
Dzsozi (h03) Posted April 16, 2020 Author Share Posted April 16, 2020 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
N3xT Posted April 16, 2020 Share Posted April 16, 2020 Check this out: https://www.youtube.com/watch?v=nHZJolSKygk https://www.youtube.com/watch?v=Kn1pBKYZFAM 1 Link to comment
Dzsozi (h03) Posted April 16, 2020 Author Share Posted April 16, 2020 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 IIYAMA Posted April 16, 2020 Moderators Share Posted April 16, 2020 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
Dzsozi (h03) Posted April 16, 2020 Author Share Posted April 16, 2020 (edited) 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 April 16, 2020 by Dzsozi (h03) Link to comment
Moderators IIYAMA Posted April 16, 2020 Moderators Share Posted April 16, 2020 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
Dzsozi (h03) Posted April 16, 2020 Author Share Posted April 16, 2020 (edited) 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". 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 April 16, 2020 by Dzsozi (h03) Link to comment
Moderators IIYAMA Posted April 16, 2020 Moderators Share Posted April 16, 2020 (edited) 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 April 16, 2020 by IIYAMA Link to comment
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now