Finding an Object in a Radius

From Mod Wiki

(Difference between revisions)
Jump to: navigation, search
Revision as of 16:56, 2 May 2010 (edit)
Alundaio (Talk | contribs)

← Previous diff
Revision as of 07:07, 28 May 2010 (edit) (undo)
Alundaio (Talk | contribs)
(Removing all content from page)
Next diff →
Line 1: Line 1:
-= 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: 
-<pre> 
- if (self.object:alive() and character_community(self.object) == "bloodsucker") then 
- if (self.object:position():distance_to_sqr(db.actor:position()) < 100 then 
- -- play sound 
- end 
- end 
-</pre> 
-Of course if you use that as is, it's going to play continuously when the bloodsucker was less then 10 units away from the player. You would have to add in a delay or a heartbeat like this. 
-<pre>  
- -- 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_sqr(db.actor:position()) < 100 ) 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 
-</pre> 
-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. 
-<pre> 
- -- 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_sqr(db.actor:position()) < 100 ) 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 
-  
-</pre>  
-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 on-line 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: 
-<pre> 
- 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 
-</pre>  
-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: 
-<pre> 
- function motivator_binder:net_spawn(sobject) 
-</pre>  
-And find '''db.add_obj(self.object)''' and put this underneath it: 
-<pre> 
- db.add_obj(self.object) 
- db.add_stalker(self) -- Added db.add_stalker(self) 
-</pre> 
-Look for: 
-<pre> 
- function motivator_binder:net_destroy() 
-</pre>  
-And find '''db.del_obj(self.object)''' and put this underneath it: 
-<pre> 
- db.del_obj(self.object) 
- db.del_stalker(self) -- Added db.del_stalker(self) 
-  
-</pre>  
-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 ''on-line'' it will store it's self into a table. This will make iterating 
-through tables for a specific object type much more efficient. 
-<pre> 
- 
- -- 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_sqr(db.actor:position()) < 100 ) 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  
-</pre> 
-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 on-line 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! 
-<pre> 
- -- 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_sqr(self.myobject:position()) 
- 
- if (dist <= 100) 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  
- 
-</pre> 
-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''' 
-<pre>class "nearest_object" 
-function nearest_object:__init() 
- self.object = nil 
- self.distance = nil 
- self.update_time = nil 
-end</pre> 
- 
-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''' 
-<pre>function nearest_object:evaluate(relative_object,tbl,radius,heartbeat,nearest,alive,community)</pre> 
- 
-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.''' 
- 
-<pre> if ( self.update_time ~= nil ) then 
- if ( time_global() >= self.update_time ) then 
- self.update_time = nil 
- else 
- return nil 
- end 
- end</pre> 
- 
-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''' 
-<pre>if ( self.update_time == nil ) then 
- local lowest_distance,object_found = nil,nil 
- local distance = nil 
-</pre> 
- 
-'''Iterating through the table''' 
-<pre> 
- 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 ( value ) ~= nil ) then 
- obj = value 
- else 
- return false 
- end 
- 
-</pre> 
-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(tonumber(key)) gives a value. Note: Since id's are always numerical. You need to use the tonumber() function on the key because it is a string. 
- 
-'''Checking conditions''' 
- 
-<pre> 
- 
- if not( alive ~= obj:alive() and character_community(obj) ~= community ) then 
-</pre> 
- 
-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 nil or true. So saying '''not ( alive ~= obj:alive() )''' is just an easier way of saying alive can be anything but false. In the end this will allow us to either search among both living and dead objects or search for object that do not have a community like anomalies. 
- 
-'''Distance and keeping the object with the lowest found distance''' 
-<pre> 
- distance = obj:position():distance_to_sqr(relative_object:position()) 
- 
- if ( distance < radius ) then 
- 
- if ( nearest == false ) then 
- self.object = obj 
- self.distance = 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</pre> 
- 
-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''' 
-<pre> 
- 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 
-</pre> 
- 
-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''' 
-<pre>function nearest_object:__finalize() 
-end</pre> 
- 
-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: 
- 
-<pre>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 ( value ) ~= nil ) then 
- obj = value 
- else 
- return false 
- end 
- 
- if not( alive ~= obj:alive() and character_community(obj) ~= community ) then 
- 
- distance = obj:position():distance_to_sqr(relative_object:position()) 
- 
- if ( distance < radius ) then 
- 
- if ( nearest == false ) then 
- self.object = obj 
- self.distance = 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</pre> 
- 
-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: 
- 
-<pre>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</pre> 
- 
-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. 
- 
-<pre>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,500,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</pre> 
- 
-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. 
- 
-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 only when that distance is pretty close to the player. Of course it really depends on what you are trying to do. 
- 
-Also as a tip. If you need to find the exact distance and you are using distance_to_sqr. You can just take the square root of the final lowest distance found. 
- 
-'''Here are a few more function examples for this class''': 
-<pre>function nearest_stalker(instance,relative_object,radius,heartbeat,alive,community) 
- return instance:evaluate(relative_object,db.stalker_by_name,radius,heartbeat,true,alive,community) 
-end 
- 
-function nearest_anomaly(instance,relative_object,radius,heartbeat) 
- return instance:evaluate(relative_object,db.anomaly_by_name,radius,heartbeat,true,nil,nil) 
-end 
- 
-function object_in_radius(instance,type,relative_object,radius,heartbeat,nearest,alive,community) 
- 
- if ( type == "monster" ) then 
- tbl = db.monster_by_name 
- elseif ( type == "stalker" ) then 
- tbl = db.stalker_by_name 
- elseif ( type == "anomaly" ) then 
- tbl = db.anomaly_by_name 
- elseif ( type == "actor_combat_enemy" ) then 
- tbl = xr_combat_ignore.fighting_with_actor_npcs 
- elseif ( type == "smart_terrain" ) then 
- tbl = db.smart_terrain_by_id 
- elseif ( type == "surge_cover" ) then 
- local surge = surge_manager.get_surge_manager 
- tbl = surge.covers 
- elseif ( type == "smart_cover" ) then 
- tbl = bind_smart_cover.registered_smartcovers 
- elseif ( type == "zone" ) then 
- tbl = db.zone_by_name 
- elseif ( type == "story_object" ) then 
- tbl = db.story_object 
- elseif ( type == "door" ) then 
- tbl = db.level_door 
- end 
- 
- if ( tbl ) then 
- return instance:evaluate(relative_object,tbl,radius,heartbeat,nearest,alive,community) 
- end 
- 
-end 
-</pre> 
- 
- 
- 
- 
---[[User:Alundaio|Alundaio]] 11:07, 25 April 2010 (EEST) 

Revision as of 07:07, 28 May 2010

Personal tools