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.
Could you post a link to the thread you found about this?
Might save some reinventing the wheel :)
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.
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.
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)
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)).
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?
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));
}
This line seems to be causing a Type Mismatch (Cannot convert 'float' to 'int'):
if (l == 0) return true
Needs to be:
if (l == 0.0) return true;
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.
Great, glad it works now :)