In the previous section, we sent "open loop" motion commands to our avatar. That means that we sent the commands blindly, hoping that the avatar would react in the planned way. The Perl script didn't "observe" the resulting motions. In this section we "close the loop" so that the script knows where it is going.
By "autopilot", we mean a Perl script that will navigate your avatar to a destination specified by its (x,y) coordinates, and then make it stop. We call an autopilot "simple" if it knows about only the left- and right-arrow keys (and maybe the down-arrow key for stopping). A simple autopilot can work for any vehicle, or for a walking or running avatar, since they all share the same conventions for these three arrow keys. To use a simple autopilot, you just put your avatar (or hoverboard, or hoverpack, or buggy) into forward motion, get cruise control turned on, and then start up the autopilot to do the steering.
The two functions that make up a simple autopilot are (1) the sensing function, which figures out whether bearing to the desired destination is to the left or right of the current heading (and by how much), and (2) the control function, which presses the left- or right-arrow keys to bring the bearing and heading into alignment. Remember: heading means which way you are going, while bearing means which direction is your intended destination.
To do the sensing function, we first need get our avatar's
doid (or "distributed object ID"). This is the number you see
attached to your avatar if you press SHIFT-CTRL-D on the
keyboard when the There window is active. You can get it
automatically from the localhost:9999 interface (did you remember
to turn it on with SHIFT-CTRL-L?) by fetching from the URL (a global
variable in the toolkit)
$URLpilot = "http://localhost:9999/ClientLoginGui/pilotInfo";
A subroutine to do this is as follows,
sub GetPilotDoid {
# return DOID of the logged-on avatar
$req = HTTP::Request->new('GET', $URLpilot);
$res = $ua->request($req);
$res->content =~ m/<PilotDoid>([0-9]+)<\/PilotDoid>/ ;
return $1;
}
Notice that although the returned string is actually in XML
syntax, it is here easy enough to parse out the PilotDoid with
a simple Perl pattern.
Next, we use another nice feature of localhost:9999, that it
will fetch the x,y,z coordinates, and the heading, of any doid
that it knows to be in your vicinity. (It is not exactly clear
what "vicinity" means here, as we will discuss later.) The URL
for this is
$URLloc = "http://localhost:9999/ihost/doblocation?doid=";,
(with a doid number appended). An encapsulating subroutine
is
sub GetThobLocation {
# returns (x,y,z,heading,altitude) for specified Thob
my $dob = shift;
my ($x,$y,$z,$head,$alt,$ret,$xmlh);
$req = HTTP::Request->new('GET', $URLloc . $dob );
$res = $ua->request($req);
$ret = $res->content;
$xmlh = XMLin($ret, ForceArray => 0); # parse returned XML
$x = $xmlh->{posX};
$y = $xmlh->{posY};
$z = $xmlh->{posZ};
$head = $xmlh->{heading};
$alt = (sqrt($x**2+$y**2+$z**2)-6000000) ;
return ($x,$y,$z,$head,$alt);
}
So if we call this with our own doid as the argument,
we'll get our location and heading. (Later, we'll use this
same routine to locate other avatars and teleport destination
objects.)Another thing worth noticing in GetThobLocation is the use of XMLin() (part of the XML::Simple package) to parse the returned XML into a Perl hash. We can then pull out posX, posY, posZ with the sytax shown.
Now suppose that we want to navigate from our current ($x,$y) and $heading to some other ($xx,$yy). We need to know how much (and which direction) to turn. The following subroutine does this calculation for us
sub HowMuchToTurn {
# returns heading correction needed to face an object at specified x,y
# positive means turn left; negative means turn right
my ($doid, $xto, $yto) = @_;
my ($x, $y, $z, $head) = &GetThobLocation($doid);
$globalmyx = $x; $globalmyy = $y;
my $b = -atan2($xto-$x,$yto-$y) * (180/3.14159) ;
if ($b < 0) {$b += 360;}
my $corr = $b - $head;
if ($corr < -180) {$corr += 360;}
if ($corr > 180) {$corr -= 360;}
return $corr;
}
All that adding and subtracting of 180 or 360 degrees is ugly, but it
works. Part of the problem is that headings in There are specified by
a uniquely ugly system, where North is 0 degrees, but East is -90
instead of the usual +90 in Real Life; and West is +90 instead of the
usual RL value of +270. Oh well. Note that global variables
$globalmyx and $globalmyy are set by this routine, for possible use by
other routines.If you are a physics geek, you might like to work out a more elegant way to do the heading/bearing arithmetic using the vector cross product of two unit vectors. (Just a hint...)
Now for the control function. You might think we should just press the left arrow key and hold it down as long as we are to the right of the intended bearing, or press the right arrow key and hold it down if we are to the left. This is a control function all right, but a poor one. The reason is that there are delays in the system, making this strategy susceptible to instability by overshooting.
Some experimenting gives this simple control function, which seems to work pretty well:
sub SteerToLocation {
# send left and right arrows to steer towards a destination
# returns distance to destination
my ($doid, $therewin, $xto, $yto) = @_;
Win32::GuiTest::SetForegroundWindow($therewin);
$sleep->Call(1000);
my $corr = &HowMuchToTurn($doid, $xto, $yto);
if ($corr > 0) {$key->Call(0x25, 0x45, 0x01, 0);} # press left
if ($corr < 0) {$key->Call(0x27, 0x45, 0x01, 0);} # press right
if (abs($corr) > 10) {$sleep->Call(150);}
else {$sleep->Call(15*abs($corr));}
if ($corr > 0) {$key->Call(0x25, 0x45, 0x03, 0);} # release left
if ($corr < 0) {$key->Call(0x27, 0x45, 0x03, 0);} # release right
return sqrt(($xto-$globalmyx)**2+($yto-$globalmyy)**2);
}
The basic idea is to travel for a second, then see how much to
turn. If the amount to turn is greater than 10 degrees in
absolute magnitude, we
hold down the appropriate arrow key for 150 milliseconds. If
it is less than 10 degrees, we instead hold it down for 15
milliseconds times the number of degrees ("proportional
feedback"). It is actually a lot of fun to watch this
algorithm zero in on the correct bearing! We return the
distance-to-destination, to make the autopilot particularly
concise (see below).When we get there, we need to stop:
sub StopMe {
# sends 2 secs of Down key five times
my $i;
$key->Call(0x26, 0x45, 0x03, 0); # release up
for ($i = 0; $i < 5; $i++) {
$key->Call(0x28, 0x45, 0x01, 0); # press down
$sleep->Call(2000);
$key->Call(0x28, 0x45, 0x03, 0); # release down
$sleep->Call(100);
}
}
So, with all this machinery, our simple autopilot takes only
a few lines:
($xto,$yto) = (YOUR DESTINATION COORDINATES HERE);
{$dist = &SteerToLocation($mydoid,$therewin,$xto,$yto);} until ($dist < 50);
&StopMe();
That's it! Try setting your buggy to go over some mountains in
There with this algorithm. It is fun to watch it rolling over,
tumbling down cliffs, then righting itself, finding the correct
heading, and then continuing. The algorithm is very persistent,
even more so if you add a line of code to keep the up arrow key pressed all the
time.
Then, if you hit a no drive zone, so that your buggy disappears on you,
your avatar (still driven by the autopilot) just keeps going towards
the destination on foot!Exercise for the reader: You also have enough machinery now to make a "follow the leader" autopilot: Give it the doid number of the leader avatar, and have your avatar automatically follow the leader a few paces behind. Wouldn't it be fun to have whole lines of avatars following each other around on autopilot? Or vee formations of hoverboats flying like geese across the sky?
Next: Part 6