Adventure Game Studio

AGS Support => Beginners' Technical Questions => Topic started by: Iphigenia on Sun 13/02/2011 23:31:21

Title: SOLVED: Vision Cone confusion
Post by: Iphigenia on Sun 13/02/2011 23:31:21
Can anyone help me add a vision cone to a patrolling NPC? I've read all I can find in the forum on the subject, but can't get my unmathematical head round it  :-[

The vector method sounds fantastic, but is beyond me unless I can paste some ready-made code straight into my script.

The situation: I have an NPC following a roughly circular patrol route made up of around 9 waypoints. I'd like to have him 'looking' directly in front of him as he goes (vision cone pointed in same direction as his direction of travel). If the player 'collides' with this vision cone there's a chance he's spotted.

Thanks in advance.

Title: Re: Vision Cone confusion
Post by: Khris on Mon 14/02/2011 18:51:24
Could you post a link to the thread you found about this?
Might save some reinventing the wheel :)
Title: Re: Vision Cone confusion
Post by: Iphigenia on Tue 15/02/2011 00:42:58
These two threads are very relevant: 

http://www.adventuregamestudio.co.uk/yabb/index.php?topic=41690.0

http://www.adventuregamestudio.co.uk/yabb/index.php?topic=38687.0

I've bodged an extremely clumsy solution using another character and the follow command (in effect the following character is the NPC and the (invisible) character in front is his gaze) It kinda works, but I'd much rather use a vision cone that pointed exactly in the direction the NPC was moving (perhaps with some randomness tossed in to simulate occasional glances left and right).

Any idiot-proof advice would be much appreciated. Thanks.
Title: Re: Vision Cone confusion
Post by: Khris on Tue 15/02/2011 09:50:57
I remember the thread; there an easier way actually.

What you need is a) the distance between NPC and player and b) the angle of the line NPC-player relative to the NPC's viewing direction.

If both are within a specified limit, the NPC will see the player.

As for the NPC occasionally glancing left and right, that makes stuff a lot more complicated, so I'll skip that for now.

The distance is calculated easily:
bool Close(this Character*, int limit) {
 int x = player.x - this.x;
 int y = player.y - this.y;
 return (x*x + y*y <= limit*limit);
}


To get the NPC's direction of walking, their coordinates have to be queried in intervals.
// those two at top of script
int npc_x[100], npc_y[100];  // previous position
int npc_xd[100], npc_yd[100];  // direction

int dir_timer;

function repeatedly_execute() {

 ....  // rest of code

 if (dir_timer >= GetGameSpeed()*2) {  // might want to try other values here
   dir_timer = 0;
   int i = 0;
   Character*c;  
   while (i < Game.CharacterCount) {
     c = character[i];
     if (c.Room == player.Room && c != player) {
       npc_xd[i] = c.x - npc_x[i];  // calculate vector from position 2 seconds ago to now
       npc_yd[i] = c.y - npc_y[i];
       npc_x[i] = c.x;
       npc_y[i] = c.y;
     }
     i++;
   }
 }
 else dir_timer++;
}


Now we can calculate the angle:
bool InView(this Character*, int limit) {
 float x1 = IntToFloat(player.x - this.x);
 float y1 = IntToFloat(player.y - this.y);
 float l = Maths.Sqrt(x1*x1 + y1*y1);
 if (l == 0.0) return true;
 x1 = x1/l;
 y1 = y1/l;

 float x2 = IntToFloat(npc_xd[this.ID]);
 float y2 = IntToFloat(npc_yd[this.ID]);
 l = Maths.Sqrt(x2*x2 + y2*y2);

 if (l == 0.0) {  // NPC isn't moving
   if (this.Loop == 0) { x2 = 0.0; y2 = 1.0; }  // set direction according to loop
   if (this.Loop == 1) { x2 = -1.0; y2 = 0.0; }
   if (this.Loop == 2) { x2 = 1.0; y2 = 0.0; }
   if (this.Loop == 3) { x2 = 0.0; y2 = -1.0; }
 }
 else {
   x2 = x2/l;
   y2 = y2/l;
 }

 float angle = Maths.RadiansToDegrees(Maths.Arcos(x1*x2 + y1*y2));

 return (angle <= IntToFloat(limit));
}


Now we can at any point do:

 if (cNpc.Close(70) && cNpc.InView(20)) ...    // if player is less than 70 pixels away and within +/-20°

Since you're probably going to do that check in rep_ex, too, you need to put the InView() function above rep_ex.
Title: Re: Vision Cone confusion
Post by: Iphigenia on Tue 15/02/2011 12:20:06

Thanks Khris.

I understand a tiny fraction of this so apologies if I'm doing something stupid:

Pasting the code into a very simple empty test room (player + one patrolling NPC) I get a "Array Index out of bounds" message on line:


c = character[i];


(index: 24 bounds: 0..23)

There was also an issue with 'Acos' in

float angle = Maths.RadiansToDegrees(Maths.Acos(x1*x2 + y1*y2));


"Not a public member of Maths"

I changed 'acos' to 'Arcos' just to see if it would run (which it did until it hit the Array Index out of bounds)
Title: Re: Vision Cone confusion
Post by: Khris on Tue 15/02/2011 12:31:02
Right, somehow I was under the impression that characters were numbered starting at 1.

Replace these three lines in rep_ex:

   int i = 0;   //   <<----- 0!
   Character*c;  
   while (i < Game.CharacterCount) {       ///   < not <=


To explain what the code does:

The rep_ex part looks at the current position of all NPCs every two seconds. The vector from the previous to the current position is then calculated and stored.
Say an NPC was at 115, 170 two seconds ago and is now at 145, 168, then the vector (30; -2) is stored for that NPC. In other words, their current viewing direction is east, with a slight tilt to the north.
Then their current position is stored as previous position, and two seconds later the direction is updated.
This should be accurate enough.

The InView function calculates the vector from the NPC to the player. This is vector 1. The second vector is the current viewing direction of the NPC.
Both of these vectors are then normalized, i.e. divided by their length. The result is a unit vector, a vector pointing in the same direction but with a length of 1.
Then the dot product is calculated, then the Acos of the result. This gives the angle between the two vectors (formula taken from here (http://www.euclideanspace.com/maths/algebra/vectors/angleBetween/index.htm)).
Title: Re: Vision Cone confusion
Post by: Iphigenia on Tue 15/02/2011 14:11:40
It seems to work brilliantly for one guard but if I add more I sometimes get a

'Error. Floating Point divide By zero.' message

on line

x2 = x2/l;

I'm reproducing this spotting check:   
if (cnameguardhere.Close(70) && cnameguardhere.InView(20))

for each guard. Could that be the problem?
Title: Re: Vision Cone confusion
Post by: Khris on Tue 15/02/2011 14:36:17
Damn, that's what happens if you code everything in a message window :)

This will occur if a) the player is at the exact same coordinates as an NPC or if an NPC isn't moving.

Currently, the InView function doesn't cope with a standing character correctly.

Here's an amended version:

bool InView(this Character*, int limit) {
 float x1 = IntToFloat(player.x - this.x);
 float y1 = IntToFloat(player.y - this.y);
 float l = Maths.Sqrt(x1*x1 + y1*y1);
 if (l == 0.0) return true;
 x1 = x1/l;
 y1 = y1/l;

 float x2 = IntToFloat(npc_xd[this.ID]);
 float y2 = IntToFloat(npc_yd[this.ID]);
 l = Maths.Sqrt(x2*x2 + y2*y2);

 if (l == 0.0) {  // NPC isn't moving
   if (this.Loop == 0) { x2 = 0.0; y2 = 1.0; }  // set direction according to loop
   if (this.Loop == 1) { x2 = -1.0; y2 = 0.0; }
   if (this.Loop == 2) { x2 = 1.0; y2 = 0.0; }
   if (this.Loop == 3) { x2 = 0.0; y2 = -1.0; }
 }
 else {
   x2 = x2/l;
   y2 = y2/l;
 }

 float angle = Maths.RadiansToDegrees(Maths.Arcos(x1*x2 + y1*y2));

 return (angle <= IntToFloat(limit));
}
Title: Re: Vision Cone confusion
Post by: Iphigenia on Tue 15/02/2011 16:41:35
This line seems to be causing a Type Mismatch (Cannot convert 'float' to 'int'):

if (l == 0) return true
Title: Re: Vision Cone confusion
Post by: DoorKnobHandle on Tue 15/02/2011 16:54:36
Needs to be:


if (l == 0.0) return true;
Title: Re: Vision Cone confusion
Post by: Iphigenia on Tue 15/02/2011 18:31:40
Cheers dkh. That did the trick.

I now have eagle-eyed guards pacing around the room!

Thanks for all the help Khris. You're a marvel.
Title: Re: SOLVED: Vision Cone confusion
Post by: Khris on Tue 15/02/2011 19:08:01
Great, glad it works now :)