Finding an Object in a Radius

From Mod Wiki

Revision as of 09:55, 11 April 2011 by Alundaio (Talk | contribs)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search

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_sqr(db.actor:position()) < 100 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 units 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_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

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_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
	

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:

	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 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.


	-- 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	

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!

	-- 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	

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 ( value ) ~= nil ) then
				obj = 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(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


		if not( alive ~= obj:alive() and 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 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

				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

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 ( 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

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,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

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:

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



--Alundaio 11:07, 25 April 2010 (EEST)

Personal tools