Finding an Object within a given Radius
From Mod Wiki
Contents |
Finding an Object Within the Radius of Another Object
Introduction
First, I would like to talk about the various methods on how to do this. Then I will present you my solution in which I think is more efficient as opposed to these other methods. To my knowledge there is no current function that will do this easily and efficient. If I am wrong in thinking such then feel free to present me the evidence that my method is not efficient in comparison. Herein are examples, most of which I tested, in which I will show you how to find an object within a radius relative to another object (db.actor in my examples).
Update function in bind_monster and xr_motivator
The usual method is to put a "distance_to_sqr" or "distance_to" check here for the object. For instance, if you wanted to play a sound when a certain monster community was a certain distance away from the player, you would put something like this:
if (self.object:alive() and character_community(self.object) == "bloodsucker") then if (self.object:position():distance_to(db.actor:position()) < 10 then -- play sound end end
Of course if you use that as is, it's going to play continuously when the bloodsucker was less then 10 meters away from the player. You would have to add in a delay or a heartbeat like this.
-- Add some local var outside the scope of the update function local search_flag = false local update_time = nil function generic_object_binder:update(delta) ... if ( search_flag == false ) then if (self.object:alive() and character_community(self.object) == "bloodsucker") then if ( self.object:position():distance_to(db.actor:position()) < 10 ) then -- play sound search_flag = true update_time = time_global() + 60 -- set next search to be in 1 second end end else -- Check if current time matches next update time if (update_time ~= nil and time_global() <= update_time) then search_flag = false update_time = nil end end
The sound will now only play every second when the bloodsucker is less then 10 meters away. Also a benefit is that it will only do the distance calculation every second, making it a little more efficient. But, there is also several things wrong in doing something like this. First, every single generic (or Monster) server object is going to run this condition. It's cut down some by checking for a certain community and if the client object is alive. Even still this is a very inefficient approach.
Searching db.storage for the object
Another approach is to run a search on the db.storage table for the object you want to check for. Usually used if you for some reason don't want it in the other binder scripts. It's usually put in the bind_stalker script.
-- bind_stalker.script -- Add some local vars outside the scope of actor update function local search_flag = false local update_time = nil function actor_binder:update(delta) ... if ( search_flag == false ) then for key, value in pairs (db.storage) do obj = db.storage[key].object if (obj and IsMonster(obj) and obj:alive() and character_community(obj) == "bloodsucker") then if ( obj:position():distance_to(db.actor:position()) < 10 ) then -- play sound search_flag = true update_time = time_global() + 60 -- set next search to be in 1 second break -- Break out of loop on first match end end end else -- Check if current time matches next update time if (update_time ~= nil and time_global() <= update_time) then search_flag = false update_time = nil end end
This looks similar to the previous method except it does things much differently. First, it will run through the entire db.storage table and run a distance calculation on each online object that matches the condition. In my opinion this is more efficient then the previous for obvious reason. It doesn't run the distance calculation on every single instance of the generic object binder (Monster server object) on each update. How can we make this more efficient?
More efficient approach
The last method I showed you that you can grab the online object from the db.storage table and match it to some conditions to get the result you want. Now I'll show you a trick on making searching the table more efficient.
Go into the db.script and add these below:
monster_by_name = {} stalker_by_name = {} function add_monster( obj ) monster_by_name[obj.object:name()] = obj end function del_monster( obj ) monster_by_name[obj.object:name()] = nil end function add_stalker( obj ) stalker_by_name[obj.object:name()] = obj end function del_stalker( obj ) stalker_by_name[obj.object:name()] = nil end
If you took the time to figure out what you just put in the db.script you'll realize you are defining two new global tables and two new functions to push an object's name into that table. Now go into xr_motivator.script.
Look for:
function motivator_binder:net_spawn(sobject)
And find db.add_obj(self.object) and put this underneath it:
db.add_obj(self.object) db.add_stalker(self) -- Added db.add_stalker(self)
Look for:
function motivator_binder:net_destroy()
And find db.del_obj(self.object) and put this underneath it:
db.del_obj(self.object) db.del_stalker(self) -- Added db.del_stalker(self)
Now go into bind_monster.script and find net_spawn() and net_destroy() adding db.add_monster(self) and db.del_monster(self) respectfully.
"What the hell are you having me do?" you may ask. Well everytime the server object for a stalker or monster goes online it will store it's self into a table. This will make iterating
through tables for a specific object type much more efficient.
-- bind_stalker.script -- Add some local vars outside the scope of actor update function local search_flag = false local update_time = nil function actor_binder:update(delta) ... if ( search_flag == false ) then for key, value in pairs (db.monster_by_name) do obj = value.object -- Value will hold the server object's name. so value.object will give us it's online client instance if (obj and obj:alive() and character_community(obj) == "bloodsucker") then if ( obj:position():distance_to(db.actor:position()) < 10 ) then -- play sound search_flag = true update_time = time_global() + 60 -- set next search to be in 1 second break -- Break out of loop on first match end end end else -- Check if current time matches next update time if (update_time ~= nil and time_global() <= update_time) then search_flag = false update_time = nil end end
If you just screamed inside "It's the same thing", well...you're wrong! The table is dramatically smaller and now we don't have to check for IsStalker(obj) or IsMonster(obj) because we are searching a specific table for what we are looking for. Let's examine further why this method is much more efficient and better then all the previous methods.
1. The conditions and the distance calculations aren't being ran on every update for each and every generic object binder (monsters) or motivator binder (stalkers).
2. Only online objects will be in the table. Only if the conditions match will the distance_to calculations be made.
3. Finding the nearest object is only a few conditions away! That's right!
Finding the NEAREST object to an object
I mentioned that finding the nearest object is really easy this way. Here's how!
-- bind_stalker.script -- Add some local vars outside the scope of actor update function local search_flag = false local update_time = nil local FOUND_OBJECT = nil local FOUND_OBJECT_DISTANCE = nil function actor_binder:update(delta) ... if ( search_flag == false ) then local dist = nil local lowest_dist = nil for key, value in pairs (db.monster_by_name) do obj = value.object -- Value will hold the server object's name. so value.object will give us it's online client instance if ( obj and obj:alive() and character_community( obj ) == "bloodsucker" ) then dist = obj:position():distance_to(self.myobject:position()) if (dist <= 10) then if (lowest_dist == nil) then lowest_dist = dist end -- Keep only the lowest found distance if (dist < lowest_dist) then FOUND_OBJECT = obj FOUND_OBJECT_DISTANCE = dist lowest_dist = dist dist = nil end end end end lowest_dist = nil -- Do stuff after table has been iterated fully -- FOUND_OBJECT hold's the object of the nearest object found. -- FOUND_OBJECT_DISTANCE hold's that objects current distance away from actor. search_flag = true else -- Check if current time matches next update time if (update_time ~= nil and time_global() <= update_time) then search_flag = false update_time = nil end end
As you can see only a few things were changed. We removed the break to allow the table to be fully iterated while keeping track of the lowest distance found, resulting in the nearest object. You may also guess that this might possibly be less efficient then just searching for an object within a radius. But in my opinion it is still more efficient then running a distance check on an update function on every single stalker or monster. Still, one must use this method sparringly in an update function.
Now the rest is up to you. Keep in mind how often you want to check for an object within a certain radius. Using longer heartbeats will lessen the workload of each update. You can take a look at the other tables in the db.script to find another object type. Like anomalies for instance. Watch when using a table that stores the object:id() instead of the name. You'll have to use alife():object(value) to get the online client object. I also suggest turning this into a function and putting it in the _G.script if you are going to use it for multiple things.
Creating a multi-purpose class
Now I'll show you how to create a nice class using what I showed you.
Setting up the class object
class "nearest_object" function nearest_object:__init() self.object = nil self.distance = nil self.update_time = nil end
Here we defined the initialization function for our class. object will hold the object that is found during the search. distance will hold the exact distance the object is at during the time of the search. update_time will be used as a heartbeat.
Create the function that will handle everything
function nearest_object:evaluate(relative_object,tbl,radius,heartbeat,nearest,alive,community)
Here we are going to pass on quite a few values that will be useful for us in creating our universal nearest_object class. relative_object will be the object in which the distance is relative to. ie. db.actor would be placed here if you are trying to find a monster nearest the actor. Tbl will specify the table in which we will lookup the object. Radius is the max distance between the relative object and the object we are searching for in the table. heartbeat is the amount of time in which we will delay in seconds for each search. nearest will be a boolean for either finding the first found match or the nearest object. alive will specify whether to search for a living object, non-living or dead object. Community will be used for searching for an object of a specific community.
Now this is were things are going to get complicated if you are new to lua. So examine what is happening carefully.
Before anything else we are going to want to check for the update time.
if ( self.update_time ~= nil ) then if ( time_global() >= self.update_time ) then self.update_time = nil else return nil end end
First we check if update_time is not empty. Then we proceed further to check if the global time in seconds is greater then our heartbeat (Which will be defined later on). If our update time expired we set update_time to nil and continue on down the script. If the time hasn't expired we will exit the function with nil.
Before we start iterating through the table, we check if update_time is nil and set a few needed locals
if ( self.update_time == nil ) then local lowest_distance,object_found = nil,nil local distance = nil
Iterating through the table
for key, value in pairs (tbl) do -- get object according to tbl type ie. by_name or by_id if ( value.object ~= nil) then obj = value.object elseif ( alife():object(value) ~= nil ) then obj = alife():object(value) else return false end
Some tables, like anomaly_by_name store the object's name into the table. Others store the id. So we need to take this into account. value.object will return not empty if the table stores the object by name. If that didn't work then we check if alife():object(value) gives a value.
Checking conditions
if ( alive == nil or alive == obj:alive() ) and ( community == nil or character_community(obj) == community ) then
In order to take into account that this class will allow us to easily search for any object in a table we must specify to check that the params alive and community can either be empty or match the alive param in order to continue with the condition. This is because when we specify nil when we are searching for an object that do not use these values we won't run into problems. Also it will allow us to search for both living and dead objects if it is set to nil.
Distance and keeping the object with the lowest found distance
distance = obj:position():distance_to(relative_object:position()) if ( distance < radius ) then if ( nearest == true ) then self.object = object_found self.distance = lowest_distance self.update_time = time_global() + heartbeat return true end if ( lowest_distance == nil ) then lowest_distance = distance end if ( distance < lowest_distance ) then lowest_distance = distance object_found = obj end end end end
Here we finally check if the distance between the object we found in the table and the relative object is lower then our radius. We put a condition in here so we can later specify if we want to search for the nearest (less efficient) or just find out if there is an object within the specified radius.
Ending our evaluate function
if ( object_found ~= nil ) then self.object = object_found self.distance = lowest_distance self.update_time = time_global() + heartbeat return true else return false end end return false end
Right after we finish iterating we'll want to check if there was actually a found object. This will only happen if nearest is set to true. If there was a match found we'll store the object and it's distance. Also we want to setup the next heartbeat.
Finalization
function nearest_object:__finalize() end
We don't really need anything here. Okay so our class is done. Now if you are actually paying attention and I haven't confused you (by not being thorough) then your class should look something like this:
class "nearest_object" function nearest_object:__init() self.object = nil self.distance = nil self.update_time = nil end function nearest_object:evaluate(relative_object,tbl,radius,heartbeat,nearest,alive,community) if ( self.update_time ~= nil ) then if ( time_global() >= self.update_time ) then self.update_time = nil else return false end end if ( self.update_time == nil ) then local lowest_distance,object_found = nil,nil local distance = nil for key, value in pairs (tbl) do -- get object according to tbl type ie. by_name or by_id if ( value.object ~= nil) then obj = value.object elseif ( alife():object(value) ~= nil ) then obj = alife():object(value) else return false end if ( alive == nil or alive == obj:alive() ) and ( community == nil or character_community(obj) == community ) then distance = obj:position():distance_to(relative_object:position()) if ( distance < radius ) then if ( nearest == true ) then self.object = object_found self.distance = lowest_distance self.update_time = time_global() + heartbeat return true end if ( lowest_distance == nil ) then lowest_distance = distance end if ( distance < lowest_distance ) then lowest_distance = distance object_found = obj end end end end if ( object_found ~= nil ) then self.object = object_found self.distance = lowest_distance self.update_time = time_global() + heartbeat return true else return false end end return false end function nearest_object:__finalize() end
Well how the hell do we use it, you ask? Here we go...a test run!
Our New Class, a test run
For this example to work you must have added my db.script suggestions above in the "More Efficient Approach" section.
Put our newly created class into the _g.script and create a new function below it, like this:
function nearest_monster(instance,relative_object,radius,heartbeat,alive,community) return instance:evaluate(relative_object,db.monster_by_name,radius,heartbeat,true,alive,community) end
This will just make it easier for use to pass on necessary variables to our new class function. notice how I added instance as a parameter. This is because we want to tell the function which instance of the class to call the function evaluate on.
Now go into bind_stalker.script and go near the end of the actor_binder:update(delta) function.
if ( search_for_monster == nil ) then search_for_monster = nearest_object() -- Create an instance of nearest_object class end if ( search_for_monster ~= nil ) then if ( nearest_monster(search_for_monster,db.actor,50,240,true,nil) == true ) then -- This scope will only run once each heartbeat create_ammo("ammo_9x39_pab9", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id(), db.actor:id(),1) -- search_for_monster.object will hold the nearest found monster object -- search_for_monster.distance will hold the distance on this object end end
First we'll want to create an instance of our class. But in order to do this only once we have to make sure it doesn't exist first. Once our new instance does exist we want to pass on some parameters to our instances evaluate function. If function returns true we know it successfully found an object that met the conditions. If you for some reason need to know the object or it's exact distance you can use the instance.object and instance.distance to retrieve these stored values.
Save the script and go in game. Run up to a monster and you should see that your actor is getting 1 ammo every 4 seconds.
Adding a Flag system
If you use a flag system you can bypass the heartbeat to do something continuously as long as the flag is true, which will be set to true until the evaluate function returns false.
--[[ ------- Class "nearest_object" --------- -------------------------------------------- ---- By: Alundaio -------------------------- --]] class "nearest_object" function nearest_object:__init() self.object = nil self.distance = nil self.update_time = nil self.flag = {current = false, last = false} end function nearest_object:evaluate(relative_object,tbl,radius,heartbeat,nearest,alive,community) if ( self.update_time ~= nil ) then if ( time_global() >= self.update_time ) then self.update_time = nil else self.flag.current = self.flag.last return false end end if ( self.update_time == nil ) then local lowest_distance,object_found = nil,nil local distance = nil for key, value in pairs (tbl) do -- get object according to tbl type ie. by_name or by_id if ( value.object ~= nil) then obj = value.object elseif ( alife():object(value) ~= nil ) then obj = alife():object(value) else self.flag = {current = false, last = false} return false end if ( alive == nil or alive == obj:alive() ) and ( community == nil or character_community(obj) == community ) then distance = obj:position():distance_to(relative_object:position()) if ( distance < radius ) then if ( nearest == true ) then self.object = object_found self.distance = lowest_distance self.update_time = time_global() + heartbeat self.flag = {current = true, last = true} return true end if ( lowest_distance == nil ) then lowest_distance = distance end if ( distance < lowest_distance ) then lowest_distance = distance object_found = obj end end end end if ( object_found ~= nil ) then self.object = object_found self.distance = lowest_distance self.update_time = time_global() + heartbeat self.flag = {current = true, last = true} return true else self.flag = {current = false, last = false} return false end end self.flag = {current = false, last = false} return false end function nearest_object:__finalize() end --[[ END CLASS --]]
It may be best for when the script has it's own delay. Ie. playing a sound faster or slower depending on distance.
Now the sky is the limit with your new class. You can do anything from finding the nearest enemy currently in combat with actor (xr_combat_ignore.fighting_with_actor_npcs), to finding objects nearest of other objects. You can keep branching off from that too. For instance finding the nearest enemy in combat with the actor's nearest smartcover. Why? who knows, maybe you want to create smarter companions by attacking the nearest enemy to the player that is not in smart cover for an easy kill.
But remember use nearest sparingly. It may be best to check if an object is within a radius (by passing false for nearest to the evaluate function) with a wide heartbeat first and then check for the nearest object if that distance is pretty close to the player.
This is the very reason I made this a class. If it was a plain function you would have a lot of trouble trying to do so.
--Alundaio 11:07, 25 April 2010 (EEST)