Category talk:Articles
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 and 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. 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.
--Alundaio 05:14, 23 April 2010 (EEST)