Jump to content
Raining6

Towards the "AI can't see through the grass" holy grail

Recommended Posts

On 2/27/2023 at 4:03 PM, lukio said:

I don't get the "The grass is not short, it completely blocks the line of sight" statement - it doesn't block line of sight? Like there is tall grass (we call it elephant grass - on Altis) that blocks line of sight and provides concealment, the short grass does not.
...interesting for those that want to use the grass as concealment - like in that old CoD mission where you crawl around.

On second thought, my "blocked by grass first person view" argument is not perfect. It is a fact of optics that if player's view of AI's eyes is blocked by grass, the AI's view of players eyes would also be blocked by grass. However, It is not a fact that player's FEET would be blocked by grass from the AI's line of sight (LOS) in that same case. So, was it justified to approximate the situation focusing purely on the LOS from players head to the AI's head? And at what distance to the AI does our approximation break down?

 

Untitled2.jpg

Calculation:

Spoiler

h_AI = 1.82 #AI soldier height
h_g = 0.6 #grass height
max_d = 200 #some max distance

def tan_angle_find (opp,adj):
    tan = opp/adj
    alpha = math.degrees(math.atan(tan))
    return alpha
def tan_length_find (alpha, opp):
    ang_rad = alpha * math.pi/180
    d1 = opp/math.tan(ang_rad)
    return d1
for dist in range(2, max_d, 10): #start, stop, step
    ang = tan_angle_find (h_AI, dist)
    safe_d = tan_length_find(ang,h_g)
    print (f'At {dist:.0f}m distance the angle is {ang:.2f}deg, the obscured by grass dist is {safe_d:.1f}m')

 

 

Result:

At 2m distance the angle is 42.30deg, the obscured by grass dist is 0.7m
At 6m distance the angle is 16.87deg, the obscured by grass dist is 2.0m
At 10m distance the angle is 10.31deg, the obscured by grass dist is 3.3m
At 30m distance the angle is 3.47deg, the obscured by grass dist is 9.9m
At 50m distance the angle is 2.08deg, the obscured by grass dist is 16.5m
At 70m distance the angle is 1.49deg, the obscured by grass dist is 23.1m
At 90m distance the angle is 1.16deg, the obscured by grass dist is 29.7m
At 110m distance the angle is 0.95deg, the obscured by grass dist is 36.3m
At 130m distance the angle is 0.80deg, the obscured by grass dist is 42.9m
At 150m distance the angle is 0.70deg, the obscured by grass dist is 49.5m
At 170m distance the angle is 0.61deg, the obscured by grass dist is 56.0m
At 190m distance the angle is 0.55deg, the obscured by grass dist is 62.6m

So, the "player's head LOS to the AI's head" approximation in our "hide in grass" scripts is realistic when the player is over 6 meters away from the AI. And yes, the 0.6m tall "short" grass hides many lengths of a human body at a typical engagement distance in Arma.  Call of Duty doesn't need to enter the argument 🙂

Share this post


Link to post
Share on other sites

My final (for now at least) "invisible wall spawning" solution to this. The advantage of the invis wall approach is that it does not affect the AI algorithms. PierreMGI's disableFSM solution is faster than mine, shorter and way cooler overall, but it does affect the AI - for instance, VCom AI become more passive and no longer aggressively flank and hunt the player.  With my walls, the AI is completely unaffected and all AI mods remain functioning, just without having X-ray vision through the grass 🙂 

 

A vid of a showcase grassy hill battle that is completely impossible/unrealistic with vanilla AI (they simply continuously shoot you in the face right through the grass), but is really quite cool now - I'm happy at least 😎 

I recommend slow movement and the use of Arma 3's (always cool, but seldom used) LCtrl+W/LCtrl+S gradual stance changes to carefully peek around the grass. Abrupt fast movements get you spotted. The VCom AI doesnt just stand there, waiting to be picked off, they move and change angles, and the grass hide'n'seek-pikaboo makes the fights much more varied still. 

EDIT: see my post below for the improved version.

 

 

The script code:

Spoiler

handled_enemies = []; 
dtangos =[];  //downed tangos 
[] spawn 
	{ while {true} do 
		{      
       
		 _enemies = player call BIS_fnc_enemyTargets;     
		 //hint STR handled_enemies;   
		 //systemChat STR _enemies; 
		 dtangos = handled_enemies - _enemies; 
		 handled_enemies = handled_enemies - dtangos; //remove dead dudes from the calculation list 
   
   
    
			{  if (!(_x in handled_enemies) ) then 
				{   
					handled_enemies append [_x];   
		
				   _x spawn
				    {	         
					  if ( !alive _this) then {terminate _thisScript}; // terminate a dead dude's thread   
			 
					  
					  while{alive _this } do 
						{             
						  //build a list of sect coords sects           
						  _a = eyePos player;             
						  _b = eyePos _this;        
						  _dist = player distance _this;        
						               
						  _wH = -0.7;    //invis wall height, make less negative to increase height
						  _losH = 0.3;  //LOS  height threshold AGL, higher = easier to make walls, but less likely to hit hills apexes. Lower = reverse.
						  _n = 30;   //into how many sections to split LOS         
						  _dcf = 30;        
						  _c_set =[];              
    
						  _createWall = objNull;   
						  _sol_found = false; //try to find solution closer to hill's apex, if cannot- go for less ambitious sol
								
   
							   
						  _sect_fn = {            
						  params ["_a","_b","_n","_dcf"];            
						  _x_sect =  _a#0+_dcf*(_b#0-_a#0)/_n;             
						  _y_sect =  _a#1+_dcf*(_b#1-_a#1)/_n;            
						  _z_sect =  _a#2+_dcf*(_b#2-_a#2)/_n;            
						  [_x_sect, _y_sect, _z_sect];            
						  };            
							  
						  for [{ _i = 0 }, { _i < _n-1 }, { _i = _i + 1 }] do
						    {              
							  _c_set = [_a, _b, _n, _i+1] call _sect_fn; //i+1 as dcf+1 to ignore Bill eyepos ;_n-1 to ignore player eyepos          
							              
							  //_sectsASL set [_i, _c_set];           
							  //_sectsAGL set [_i, ASLToAGL _c_set];           
							  _thASL = getTerrainHeightASL [_c_set#0, _c_set#1];           
							   
							   _thresh = _c_set#2 - _thASL; //threshold set at the actual grass height level
							   
							   if ( (abs _thresh) < _losH && _dist>10 && "grass" in toLowerANSI (STR surfaceType (_c_set))) then //try to pick the lowest LOS above ground, e.g. closer to hill apex
							    { 
									//systemChat "Short grass!";
									_vec2e = _this getDir player;            
									_tgtAGL = _c_set;
									_tgtAGL set [2, _wH];
								   _createWall = createVehicle["GalleryLabel_01_F",  _tgtAGL, [], 0, "CAN_COLLIDE"];      
								     
								   _createWall setDir _vec2e;   
								   _createWall setVectorUp surfaceNormal position _createWall;   
								   player disableCollisionWith _createWall;   
								   _createWall setObjectScale 12;
								   _createWall setObjectTexture [0,""]; //comment out to make the walls visible 								   
								   _sol_found = true;
								   break;
								};
									
						    };           
							
						if (_sol_found==false) then //if the above fails, create a wall at a higher point
						{
							for [{ _i = 0 }, { _i < _n-1 }, { _i = _i + 1 }] do
							{              
							  _c_set = [_a, _b, _n, _i+1] call _sect_fn; //i+1 as dcf+1 to ignore Bill eyepos ;_n-1 to ignore player eyepos          
										  
							  //_sectsASL set [_i, _c_set];           
							  //_sectsAGL set [_i, ASLToAGL _c_set];           
							  _thASL = getTerrainHeightASL [_c_set#0, _c_set#1];           
							   
							   _thresh = _c_set#2 - _thASL; //threshold set at the actual grass height level
							   
							   if ( (abs _thresh) < (_losH+0.2) && _dist>10 && "grass" in toLowerANSI (STR surfaceType (_c_set))) then
								{ 
									//systemChat "Long grass!";
									_vec2e = _this getDir player;            
									_tgtAGL = _c_set;
									_tgtAGL set [2, _wH];
								   _createWall = createVehicle["GalleryLabel_01_F",  _tgtAGL, [], 0, "CAN_COLLIDE"];      
									 
								   _createWall setDir _vec2e;   
								   _createWall setVectorUp surfaceNormal position _createWall;   
								   player disableCollisionWith _createWall;   
								   _createWall setObjectScale 12;
								   _createWall setObjectTexture [0,""]; //comment out to make the walls visible 								   
								   _sol_found = true;
								   break;
								};
									
							}; 
						};
							
							
								 
							sleep 1;
							if (!isNull _createWall) then { deleteVehicle _createWall; _createWall = objNull;}
							
						};
	
				    };   

				};      
		 
			} forEach _enemies;
		 sleep 2;	
		};

	}; 

 

Many thanks to @pierremgi , @johnnyboy and @EO for  your help, I may not have done it without you!

  • Like 4

Share this post


Link to post
Share on other sites

Great work dude.  Way to tough it out!

 

Edit: @Raining6  I didn't see any shadow...how was that solved?

  • Haha 1

Share this post


Link to post
Share on other sites

@johnnyboy Ah, the shadow! i remembered to thank EO for suggesting the solution, but forgot to include the actual little mod file with it 😄 To remove the invisible walls' shadows, place this mod into steamapps/common/Arma3/!Workshop/

and activate as "Local mod" https://app.box.com/s/8celqpuft0qljj4m2oudzbmejt0sd1eb

  • Thanks 1

Share this post


Link to post
Share on other sites

Slight further revision of the above "final" version (no such thing as "final" in nature :)) . Previously, the list of enemies was built from units that were already in combat mode. The walls were spawned only after they entered combat mode. This was problematic as they saw the player through the grass without any walls, opened fire and only then lost direct sight because of the newly spawned wall. By which time the player was sometimes injured or dead.

 

In the current revision, I select enemies before they enter combat and it is thus easier (as well as more realistic) to dictate the start of the fight by using grass cover even before the start of the firefight.

 

Code:

Spoiler


handled_enemies = []; 
dtangos =[];  //downed tangos 
[] spawn 
	{ while {true} do 
		{      
       
		  _allEnemies = allUnits select {side _x getFriend playerSide <0.6}; 
		  _enemies = []; //enemies near player
		  {if ((player distance _x)  < 300) then {_enemies append [_x]} } forEach _allEnemies;    
		 hint STR handled_enemies;   
		 systemChat STR _enemies; 
		 dtangos = handled_enemies - _enemies; 
		 handled_enemies = handled_enemies - dtangos; //remove dead dudes from the calculation list 
   
   
    
			{  if (!(_x in handled_enemies) ) then 
				{   
					handled_enemies append [_x];   
		
				   _x spawn
				    {	         
					  if ( !alive _this) then {terminate _thisScript}; // terminate a dead dude's thread   
			 
					  
					  while{alive _this } do 
						{             
						  //build a list of sect coords sects           
						  _a = eyePos player;             
						  _b = eyePos _this;        
						  _dist = player distance _this;        
						               
						  _wH = -0.7;    //invis wall height, make less negative to increase height
						  _losH = 0.3;  //LOS  height threshold AGL, higher = easier to make walls, but less likely to hit hills apexes. Lower = reverse.
						  _n = 30;   //into how many sections to split LOS         
						  _dcf = 30;        
						  _c_set =[];              
    
						  _createWall = objNull;   
						  _sol_found = false; //try to find solution closer to hill's apex, if cannot- go for less ambitious sol
								
   
							   
						  _sect_fn = {            
						  params ["_a","_b","_n","_dcf"];            
						  _x_sect =  _a#0+_dcf*(_b#0-_a#0)/_n;             
						  _y_sect =  _a#1+_dcf*(_b#1-_a#1)/_n;            
						  _z_sect =  _a#2+_dcf*(_b#2-_a#2)/_n;            
						  [_x_sect, _y_sect, _z_sect];            
						  };            
							  
						  for [{ _i = 0 }, { _i < _n-1 }, { _i = _i + 1 }] do
						    {              
							  _c_set = [_a, _b, _n, _i+1] call _sect_fn; //i+1 as dcf+1 to ignore Bill eyepos ;_n-1 to ignore player eyepos          
							              
							  //_sectsASL set [_i, _c_set];           
							  //_sectsAGL set [_i, ASLToAGL _c_set];           
							  _thASL = getTerrainHeightASL [_c_set#0, _c_set#1];           
							   
							   _thresh = _c_set#2 - _thASL; //threshold set at the actual grass height level
							   
							   if ( (abs _thresh) < _losH && _dist>10 && "grass" in toLowerANSI (STR surfaceType (_c_set))) then //try to pick the lowest LOS above ground, e.g. closer to hill apex
							    { 
									//systemChat "Short grass!";
									_vec2e = _this getDir player;            
									_tgtAGL = _c_set;
									_tgtAGL set [2, _wH];
								   _createWall = createVehicle["GalleryLabel_01_F",  _tgtAGL, [], 0, "CAN_COLLIDE"];      
								     
								   _createWall setDir _vec2e;   
								   _createWall setVectorUp surfaceNormal position _createWall;   
								   player disableCollisionWith _createWall;   
								   _createWall setObjectScale 12;
								   _createWall setObjectTexture [0,""]; //comment out to make the walls visible 								   
								   _sol_found = true;
								   break;
								};
									
						    };           
							
						if (_sol_found==false) then //if the above fails, create a wall at a higher point
						{
							for [{ _i = 0 }, { _i < _n-1 }, { _i = _i + 1 }] do
							{              
							  _c_set = [_a, _b, _n, _i+1] call _sect_fn; //i+1 as dcf+1 to ignore Bill eyepos ;_n-1 to ignore player eyepos          
										  
							  //_sectsASL set [_i, _c_set];           
							  //_sectsAGL set [_i, ASLToAGL _c_set];           
							  _thASL = getTerrainHeightASL [_c_set#0, _c_set#1];           
							   
							   _thresh = _c_set#2 - _thASL; //threshold set at the actual grass height level
							   
							   if ( (abs _thresh) < (_losH+0.2) && _dist>10 && "grass" in toLowerANSI (STR surfaceType (_c_set))) then
								{ 
									//systemChat "Long grass!";
									_vec2e = _this getDir player;            
									_tgtAGL = _c_set;
									_tgtAGL set [2, _wH];
								   _createWall = createVehicle["GalleryLabel_01_F",  _tgtAGL, [], 0, "CAN_COLLIDE"];      
									 
								   _createWall setDir _vec2e;   
								   _createWall setVectorUp surfaceNormal position _createWall;   
								   player disableCollisionWith _createWall;   
								   _createWall setObjectScale 12;
								   _createWall setObjectTexture [0,""]; //comment out to make the walls visible 								   
								   _sol_found = true;
								   break;
								};
									
							}; 
						};
							
							
								 
							sleep 1;
							if (!isNull _createWall) then { deleteVehicle _createWall; _createWall = objNull;}
							
						};
	
				    };   

				};      
		 
			} forEach _enemies;
		 sleep 2;	
		};

	}; 

 

 

Share this post


Link to post
Share on other sites

Good work dude.  Now that you got the arma scripting bug, what are you gonna tackle next?  🤪

btw, your sketch skills are right up there with mine!  Haha.

  • Like 1

Share this post


Link to post
Share on other sites
14 hours ago, johnnyboy said:

btw, your sketch skills are right up there with mine!  Haha.

Function over form any day. Ok, I lie, but you know what I mean 😄

14 hours ago, johnnyboy said:

Now that you got the arma scripting bug, what are you gonna tackle next?  🤪

No idea! My second biggest gripe - no replays on helicopter flying in Arma is kinda sorted by A3RR mod and my single line hack... Not sure what next. Build ChatGPT? It's not really Arma though haha

Sorting bugs with this mod will last me a while it looks like. I found another big one - 

player disableCollisionWith _createWall; 

that I heavily relied on in my mod, it only works with one object at a  time. So one invisible wall I can ghost through without noticing, but all the others that are being spawned at the same time are quite literally invisible walls 😣
Here is a quick hacky solution, this is now my "Final" version 🙂

Code:

Spoiler

[] spawn{while {true} do
	
	{_pspeed = speed player;
	
	 if ( _pspeed >14) then {_list = position player nearObjects ["GalleryLabel_01_F", 7]; {deleteVehicle _x} forEach _list};
	 
	 if ( _pspeed >10) then {_list = position player nearObjects ["GalleryLabel_01_F", 5]; {deleteVehicle _x} forEach _list};
	 
	 if ( _pspeed >5) then {_list = position player nearObjects ["GalleryLabel_01_F", 2]; {deleteVehicle _x} forEach _list};
	 	 
	 sleep 0.2;
	}

};


handled_enemies = []; 
dtangos =[];  //downed tangos 
[] spawn 
	{ while {true} do 
		{      
       
		  _allEnemies = allUnits select {side _x getFriend playerSide <0.6}; 
		  _enemies = []; //enemies near player
		  {if ((player distance _x)  < 300) then {_enemies append [_x]} } forEach _allEnemies;    
		 //hint STR handled_enemies;   
		 //systemChat STR _enemies; 
		 dtangos = handled_enemies - _enemies; 
		 handled_enemies = handled_enemies - dtangos; //remove dead dudes from the calculation list 
   
   
    
			{  if (!(_x in handled_enemies) ) then 
				{   
					handled_enemies append [_x];   
		
				   _x spawn
				    {	         
					  if ( !alive _this) then {terminate _thisScript}; // terminate a dead dude's thread   
			 
					  
					  while{alive _this } do 
						{             
						  //build a list of sect coords sects           
						  _a = eyePos player;             
						  _b = eyePos _this;        
						  _dist = player distance _this;        
						               
						  _wH = -0.7;    //invis wall height, make less negative to increase height
						  _losH = 0.3;  //LOS  height threshold AGL, higher = easier to make walls, but less likely to hit hills apexes. Lower = reverse.
						  _n = 30;   //into how many sections to split LOS         
						  _dcf = 30;        
						  _c_set =[];              
    
						  _createWall = objNull;
						  
						  _sol_found = false; //try to find solution closer to hill's apex, if cannot- go for less ambitious sol
								
   
							   
						  _sect_fn = {            
						  params ["_a","_b","_n","_dcf"];            
						  _x_sect =  _a#0+_dcf*(_b#0-_a#0)/_n;             
						  _y_sect =  _a#1+_dcf*(_b#1-_a#1)/_n;            
						  _z_sect =  _a#2+_dcf*(_b#2-_a#2)/_n;            
						  [_x_sect, _y_sect, _z_sect];            
						  };            
							  
						  for [{ _i = 0 }, { _i < _n-1 }, { _i = _i + 1 }] do
						    {              
							  _c_set = [_a, _b, _n, _i+1] call _sect_fn; //i+1 as dcf+1 to ignore Bill eyepos ;_n-1 to ignore player eyepos          
							              
							  //_sectsASL set [_i, _c_set];           
							  //_sectsAGL set [_i, ASLToAGL _c_set];           
							  _thASL = getTerrainHeightASL [_c_set#0, _c_set#1];           
							   
							   _thresh = _c_set#2 - _thASL; //threshold set at the actual grass height level
							   
							   if ( (abs _thresh) < _losH && _dist>10 && "grass" in toLowerANSI (STR surfaceType (_c_set))) then //try to pick the lowest LOS above ground, e.g. closer to hill apex
							    { 
									//systemChat "Short grass!";
									_vec2e = _this getDir player;            
									_tgtAGL = _c_set;
									_tgtAGL set [2, _wH];
								   _createWall = createVehicle["GalleryLabel_01_F",  _tgtAGL, [], 0, "CAN_COLLIDE"];      
								    player disableCollisionWith _createWall;  
								   _createWall setDir _vec2e;   
								   _createWall setVectorUp surfaceNormal position _createWall;   
								     
								   _createWall setObjectScale 12;
								   
								   _createWall setObjectTexture [0,""]; //comment out to make the walls visible 								   
								   _sol_found = true;
								   break;
								};
									
						    };           
							
						if (_sol_found==false) then //if the above fails, create a wall at a higher point
						{
							for [{ _i = 0 }, { _i < _n-1 }, { _i = _i + 1 }] do
							{              
							  _c_set = [_a, _b, _n, _i+1] call _sect_fn; //i+1 as dcf+1 to ignore Bill eyepos ;_n-1 to ignore player eyepos          
										  
							  //_sectsASL set [_i, _c_set];           
							  //_sectsAGL set [_i, ASLToAGL _c_set];           
							  _thASL = getTerrainHeightASL [_c_set#0, _c_set#1];           
							   
							   _thresh = _c_set#2 - _thASL; //threshold set at the actual grass height level
							   
							   if ( (abs _thresh) < (_losH+0.2) && _dist>10 && "grass" in toLowerANSI (STR surfaceType (_c_set))) then
								{ 
									//systemChat "Long grass!";
									_vec2e = _this getDir player;            
									_tgtAGL = _c_set;
									_tgtAGL set [2, _wH];
								   _createWall = createVehicle["GalleryLabel_01_F",  _tgtAGL, [], 0, "CAN_COLLIDE"];      
									player disableCollisionWith _createWall; 
								   _createWall setDir _vec2e;   
								   _createWall setVectorUp surfaceNormal position _createWall;   
								     
								   _createWall setObjectScale 12;
								   
								   _createWall setObjectTexture [0,""]; //comment out to make the walls visible 								   
								   _sol_found = true;
								   break;
								};
									
							}; 
						};
							
							
								 
							sleep 1;
							if (!isNull _createWall) then { deleteVehicle _createWall; _createWall = objNull;}
							
						};
	
				    };   

				};      
		 
			} forEach _enemies;
		 sleep 2;	
		};

	}; 

 

PS The mod is now on Steam Workshop: https://steamcommunity.com/sharedfiles/filedetails/?id=2946868556 , local thread is here:  https://forums.bohemia.net/forums/topic/241678-acstg-ai-cannot-see-through-grass/

  • Like 3

Share this post


Link to post
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now

×