DoctorGibbs' Perl Toolkit for Interacting with the There World

Part 5. Closing the Loop: A Simple Autopilot

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