This is the first tutorial in the series. I expect to put out several of these per week, at least for a few weeks. BTW, let me remind you that CNTRL0.CPP was intended as a tutorial example of a robot driver. (Some people haven't noticed that, from their questions.) So study it, modify it, try to make it faster. Ask questions about it. What follows here today will be even simpler than CNTRL0.CPP: This article is short. I suggest you re-read it as many times as necessary if it is not clear the first time.
When the robot "driver" function is called by the main program, his car may be anywhere on the track. It must determine whether it is on a curve or a straight, whether it needs to accelerate or slow down, whether it is going off the track to the left or right. The basic scheme is first to determine what's happening and then to choose or calculate the control outputs. The outputs of course are the tire speed, vc, and the drift angle, alpha. (more below) It should be clear that a racing car needs to accelerate at the beginning of a straightaway, slow down for a corner, and then go around the corner as fast as the traction permits. Beyond that, it should try to take a path that hugs the inside of the turns, or a better path than that, if you can find it. The outputs vc and alpha must always move the car towards its intended path and speed. Let's follow a car around the track from the beginning of the race. The robot doesn't have to know that the race is just starting; he looks at s.to_end and sees that it is almost as big as s.cur_len. Also, he sees that s.cur_rad is zero. From those things he knows that he is near the beginning of a straightaway, and therefore he should give it full throttle. (We are assuming that he is pointed in the right direction.) Now let's look in detail at these items:
"s" is the drivers copy of the "situation" structure. The items we
will talk about are:
|s.cur_rad||is the radius of the track segment we are on. Zero means straight.|
|s.cur_len||is the length of the segment, in feet for a straightaway.|
|s.to_end||is the distance to the end of the segment, ft. for a straight.|
|s.v||is the car's current speed in feet per second, fps.|
Here is possible code to implement the above logic:
// If we are on a straightaway,
if(s.to_end > .4 * s.cur_len) // if we are far from the end,
vc = s.v + 50.0; // pedal to the metal!
The .4 could be replaced by the name of a constant, such as maybe LARGE_FRACTION, so that the second line of code tests to see if the car is a LARGE_FRACTION of the total length from the end of the straight. How do we know that .4 is right? We don't! LARGE_FRACTION is a parameter that must be determined by trial and error. The fact that we have this puzzling parameter is a clue that this algorithm is not the best, which we will talk more about later.
The same could be said for the 50.0, except that here there is some theory to guide us. First, let me define vc: vc is the speed of the bottom of the tire relative to the car itself. It is what the car's speedometer would read if it were mechanically connected to the wheel. The 50.0 is the amount of wheel slip; we are asking the simulator to spin the wheels against the track at 50 fps. We know that the car doesn't have enough power to do that, but we also know that the simulation will figure out that we want full power, and it will calculate a new vc value that just gives us 100 % power.
Why do we put 50.0 instead of 1,000,000? Well, that might work fine, I haven't tested it. It will slow down the simulation because it has to search for the correct value of vc, and starting with a ridiculous value will make the search take longer. However, we do have an excellent search method, so it might work OK.
But you need to understand how tire force and power are related, so lets see how much wheel spin you can get with our car. In order for the next paragraph to make sense there are two items to emphasize:
Mechanical power is equivalent to force times speed, if the force is pushing something
along at that speed. In this case the tire is rubbing against the track with a force
F, F being the frictional force of tire vs. track. The speed with which this force
is being delivered is vc. Then P = F * vc is the power delivered to the tire.
The main thing to notice from this relation is that force must decrease with speed, and
the greatest force you can get would be at the slowest vc. Now if you look at our
friction model in CARZ.CPP, you can see that the largest coefficient of friction we can
get is a little less than MYU_MAX, which has the value of 1.0. Hence the largest
force is about equal to the weight of the car, which is 80 *32.2 = 2576 lb.
(See the beginning of CARZ.CPP.) P is 100,000 ft. lb. per second, so if P were being
delivered with that force the tire speed would be:
vc = P/F = 100,000/2576 = 38.8 fps.
Since spinning the wheel rapidly will generate 2576 pounds of friction force, we don't have enough power for vc to exceed 38.8 fps, when the car is stationary. As the car picks up speed vc can increase, but it can't increase as rapidly as s.v, because the force will go down. A smaller force means a much smaller slip rate, i.e. vc closer to s.v. Therefore, the formula vc = s.v + 50.0 is always a request for more power than the car has, and is equivalent to full throttle.
What about steering? The car might have entered this straightaway from the
preceding turn, and not be moving straight down the track. We must output a reasonable
value of vc and alpha no matter what situation the car is in. If we find the car too
far to the right or left we must steer it away from the nearby rail. Those
conditions are detected by examining s.to_lft and s.to_rgt. More important is the
direction of motion, since it doesn't matter if the car is almost touching the rail if it
is not moving toward the rail. The direction of motion is given by s.vn, or perhaps
by the ratio of s.vn to s.v, since the angle between the cars path and the track is the
angle whose sine is s.vn/s.v. First some definitions:
|s.vn||is the component of the cars velocity normal to the track direction, fps. It is positive if the car is moving to the left.|
|s.to_lft and s.to_rgt||are the distance in feet to the track rails.|
Their sum will be a constant, the track width.
alpha is the drift angle of the car; the angle between the car's centerline and its velocity vector. At high speed a very small drift angle causes a large lateral force. At low speed a large angle produces only a moderate lateral force. We measure the angle in radians. By a small angle I mean near .01, by a large angle I mean more than .15. If the car is moving at constant speed, but changing its direction, the slip rate will be vc * sine(alpha). The slip rate in turn determines the lateral force. (In this particular situation all of the slippage is directly sideways.) One easy way to steer the car, which has been used in most of the cars so far, is to pick a distance from one wall, and then set up a "servo" to always go back to that distance. Suppose we let the variable "lane" be that desired distance from the left wall. Then when s.to_lft == lane things are just the way we want them. We can then set up a formula to always try to reach this condition like so:
alpha = STEER_GAIN * (s.to_lft - lane);
Notice that when s.to_lft is equal to lane then alpha will be zero, which is what we want. Also, when s.to_left is greater than lane, alpha will be positive, which causes a force to the left, again what we want. The STEER_GAIN factor will have to be determined by trial and error. You might want to make it smaller at higher speeds.
This is not quite good enough by itself. Servos have a tendency to oscillate, and are usually provided with damping. This is easily accomplished by putting in a term proportional to s.vn, since s.vn is a measure of how rapidly s.to_lft is changing. Our complete servo-steering formula is then:
alpha = STEER_GAIN * (s.to_lft - lane) - DAMP_GAIN *
This just says that if the car is moving rapidly toward the left, then you slide with the car pointing to the right of its direction of motion. DAMP_GAIN must be found by trial and error; it also might depend on s.v. On straightaways the damping term is more important than the lane-holding term, because as long as s.v is held near zero, it may not be important where you are on the track.
Next time we will see what to do when s.to_end is not so big anymore.