Sunday, June 18, 2023

Procedural City Stop Signs

Up to this point, 3DWorld's procedural cities have traffic lights at every 3-way and 4-way intersection for both roads inside the city and connector roads between cities. This works well for cities that are full of cars, but it may be overkill for residential neighborhoods with lighter traffic. Stop signs may be a better fit for these situations. I've added stop signs to residential cities, except for the intersections with roads connecting to other cities, which continue to use traffic lights.

It was easy to find a stop sign texture to use. Each sign is a two sided textured quad with the red "STOP" texture on one side and a white octagon on the other. The sign is placed on a square post, with an additional smaller "4-WAY" sign underneath the main sign at 4-way intersections. These are placed at all corners facing cars that will be entering the intersection. Stop signs are positioned close to where the traffic lights would be and can mostly use the same pedestrian and player collision detection system.

The next problem was adding street signs. I really like how every road has a unique procedurally generated name, but there's no traffic light to attach the signs to. It doesn't look right adding the street sign to the top of the stop sign with a vertical extension. So instead I added a second, taller vertical pole just behind the stop sign with the street name on a green horizontal banner at the top. I had to make this high enough that the largest vehicle (the truck) can pass underneath.

Here is an example stop sign on a 4-way intersection with cars and pedestrians enabled.

A 4-way stop sign with a street sign above and behind it. Cars and people are both waiting to cross.
 

The most difficult step was writing the traffic logic that controls cars reaching stop signs. As with traffic lights, the only real function of this stop sign logic is to tell each car that's about to enter the intersection whether it must stop or can proceed. Once the car is in the intersection, it's free of the intersection control logic and only subject to the car and pedestrian collision checks.

The first step is obvious: Each vehicle must stop at the stop sign, independent of the intersection state. This is accomplished by braking/decelerating when approaching the intersection, starting a timer when fully stopped, and waiting until 0.25s has passed before querying the intersection for permission to enter. This small time delay is enough to prevent a rolling stop. I may decide to make the wait randomly generated per-vehicle to add some variation.

The next step is to determine whose turn it is. We conveniently already have a timer recording when each car reached the intersection for stopping purposes. Each car will then check if there are any other waiting cars with earlier times that should enter first. This check only applies to cars that will cross paths, using the following logic:

  • If cars start at the same location, this is an error.
  • If cars end at the same destination (one of the 3 or 4 exit directions), they definitely cross.
  • If either car is turning right, they don't cross.
  • If either car is turning left, they cross.
  • If we got here, both cars are going straight. They cross if they're going in different axes (one north-south and the other east-west).

I later discovered that the trucks make wider right turns and will in some cases clip through other vehicles making left turns. So I had to add an extra step into the logic:

  • If either one is a truck, one is turning right, the other is turning left, and their paths are diagonally opposite each other, then they cross.

This set of cases appears to be correct. But it's actually more complex than this, because we also have to consider other cars currently in the intersection. My solution was to have cars register themselves with the intersection when entering and remove themselves when exiting. The intersection has 3 or 4 slots corresponding to its entry points that store the turn direction and is_truck flag for each car that's using it. Then the intersection must run the same logic above on any active slots that cross the path of the current car that is asking for permission to enter.

The next problem I ran into was a deadlock case where cars were waiting at each stop sign but no one was going. This took me a long time to reproduce, debug, and fix. I eventually had to add code to detect this case by looking for cars waiting for more than 60s, show where it was happening in the world, and print debug state for the intersection. It turned out that this was caused by cars blocking the intersection because there was no space for them to turn and exit the intersection. This was because cars were queuing up waiting at a traffic light to exit the city, and the line of cars was backing up all the way to the intersection. This in turn was causing the car attempting to enter the intersection to stop while partially inside. The problematic car had already set the "entering intersection" flag for that lane/direction as it had started to move. But when transitioning back to a "waiting" state while space for it to turn became available, it was actually blocking itself from continuing when it was able to. The fix was to have a car clear it's own intersection usage state when stopping while inside and intersection. This will also allow other cars to continue while one car is blocked, which may or may not be correct. I think this is okay because we know they can't pass the initial car as the destination spot is known to be blocked by another car. Therefore, they must be waiting as well, and only one car can claim any spot that opens up.

This overall solution mostly works. There are still some rare cases when cars spawn inside an intersection, or a lag in frame rate causes them to cross through an intersection without properly registering a matching enter + exit event. If left unchecked, this can lead to deadlock where none of the cars can enter. I think one correct fix is to record time stamps for entry events and automatically remove them after a few seconds in the per-frame logic update pass of each intersection. However, I haven't seen this happen after some other tweaks, so it's either fixed now or very rare. I may have to let the simulation run for hours in the background to see if anything fails with the stop sign logic.

Here is a video showing cars following stop sign rules at an intersection. The only issue is that trucks cal still get too close to cars when turning. This can lead to odd behavior as they attempt to avoid collisions that shouldn't actually be possible. I haven't come up with a good fix for this, so I'm calling it done for now.



5 comments:

  1. For those "flag style" mounted street name signs, I think they are supposed to stick out the other way, so they don't overhang the street. Page 38 of the Manual on Uniform Traffic Control Devices for Streets and Highways has a handy diagram.
    https://mutcd.fhwa.dot.gov/pdfs/2009/mutcd2009edition.pdf
    Looks like 2 feet is the minimum clearance. If you want it mounted overhead, 17' is the recommended clearance.

    I'm watching the video frame-by-frame, and can't detect any deceleration. Are the breaks either full on or full off? Would be a nice touch to have a longer deceleration, and maybe some variation between vehicles. The frame rocking around on the suspension a little perhaps? Though, with mathematically perfectly smooth roads, I guess you wouldn't need a suspension at all!

    ReplyDelete
    Replies
    1. I didn't know there were such specific standards on street signs. Some of the reference images have the sign going over the road. Maybe it's different of residential streets. This should be easy to change, and would avoid the problem of large trucks hitting the signs. I'm not sure why I didn't think of that. Maybe there was some reason why it didn't work.

      Cars have two functions that control stopping, "decelerate" and "decelerate_fast". The first one is used when approaching an intersection, while the second one is used when there's a pending collision. They both apply a gradual deceleration, but it's possible the fast deceleration actually stops in 1-2 frames. Or maybe it got broken at some point. Maybe the cars are always detecting the car ahead and fast decelerating. I'll have to debug it. Deceleration and acceleration both scale with max_speed, which is slightly different per vehicle. Rocking may be difficult given how the system is implemented.

      Delete
    2. Some of your suggestions like the sign scrolling direction were easy to fix. Fixing the car deceleration was quite difficult. My first attempt to reduce deceleration 10x had ... no effect? It turns out the problem is that cars go into "sleep" mode when the reach the intersection and don't wake up until either the stop time (0.25s) has expired or the light turns green. The car position is never updated in the sleep state, even if it's speed is nonzero.

      My next attempt was to continue to run the position update in sleep state. As I suspected, it resulted in cars stopping partially in the intersection and blocking traffic. That's no good.

      What I really need to do is to have cars decelerate *before* reaching the intersection. I added code to calculate the number of frames to reach the intersection and decelerate such that the car stops exactly at the line. This sometimes worked. But when there was frame time variation, the timing estimates were off and the car either stopped short (not triggering the intersection's "car sensor"), or stopped in the intersection (blocking traffic again).

      The next attempt was to calculate the distance to the intersection and apply a max speed cap when the car was within about 1 car length. I used a linear function, but if it ended exactly at the line the car would asymptotically approach the intersection and never reach it. So I added a 10% offset so that the target was 10% of max speed when the intersection was reached, which is a good approximation of stopping due to static (vs. kinetic) friction of the tires. This appears to work well. My only complaint is that this is probably the only place in the code where I bypass the car accelerate/decelerate/turn/etc. API and directly set the internal speed variable.

      Delete
    3. Intersections strike again! Yeah, that stuff is tricky, even for humans. I don't suppose that, if a car comes to a stop sign and has an open path, it proceeds without coming to a full stop? Does the 1/10 speed limit go straight to evaluation? Or does the intersection still impose a quarter second "sleep" before an update?

      Delete
    4. I put in code to avoid "rolling stops" because It looked odd. That was back when cars stopped in a single frame. I think it should look better now that the stops are gradual, so I can have some cars fail to fully stop.

      The 1/10 speed limit means cars will actually stop moving (sleep) when their speed reached 10% of their normal travel speed. I need to use some nonzero value to make the system stable. This simulates that shaking when static friction of the tires and brakes overcomes momentum and a car comes to a stop.

      Delete