So I got back to my Carvatar project, after having a little break. It currently has a simple AI controller (really simple) that actually uses only 2 raycast sensors to take input decisions or a neural network controller that is currently trained just for testing purposes after the simple AI.
The neural network manages to reproduce the simple AI really well (but that’s to be expected, because the simple AI is really simple :) ). The car model is not that hard to control so simple ruled AI does not perform that bad :). This will change.
Back to our tracks
I used procedural generated race tracks to make my life easy, this way I could have a ton of tracks without having to paint them/edit them manually. I kept the algorithm as simple as possible (my goal was just to get something ok for the AI), but it was kind of fun so I think I will later come back to this and experiment other ways of doing it.
I knew I did not want to do a lego like method (where you have a certain number of pieces for the track) and then do an assembly process :), because I want something more interesting and flexible that isn’t based on manually created content. The LEGO (L-System :D ) approach is actually a good one, used a lot in games, it makes life easier, and if you have a deadline and good graphic artists/editors I think it is the sane solution, but it’s always fun to try new things.
My first instinct was use a composite Bezzier curve, but I said I should first try something easy and see if that gets me close to what I want.
So my algorithm is simple, the trick is actually to make a radial representation of the points on the track. I first generate the center line (line that marks the center of the track), this is actually the basis of the algorithm, and then generate all the other in game information for it. If I ever want to test other generation methods I can easily change the centerline function.
We have a circle and we want to pinch it or squish it, do some random transformations so we get a random closed curve. Of course that we could actually use any kind of closed curve as a starter point (the circle is just one of the possibilities).
Pick circle center: \(C\)
For each control point
Pick a random radius \(R\) (for a low probability pick \(R\) to be really big), this will be our curvature
Map circle \(\alpha\) angle to current control point
Set the control points coordinates \(P = C + R (\sin(\alpha), \cos(\alpha))\)
Do a linear interpolation between control points to get the other points values
voidCTrack::genCenterline(b2Vec2*points){b2Vec2centerOfTrack=b2Vec2(SCREEN_SIZE_WIDTH/2.0f,SCREEN_SIZE_HEIGHT/2.0f);// generate first important points// with a low sample rate (given by the downstep)inti;for(i=0;i<m_settings.track_size;i+=m_settings.down_step){floatrandomRadius=randomInterval(m_settings.radius_curvature[0],m_settings.radius_curvature[1]);floatrandomValue=randomInterval(m_settings.hard_curvature_probability[0],m_settings.hard_curvature_probability[1]);floatoffsetconst=(randomValue>=m_settings.hard_curvature_probability[2])?(float)m_settings.hard_curvature_value:(float)m_settings.radius_offset_curvature;floatradius=offsetconst+randomRadius;floatalpha=((360.0f/(m_settings.track_size-1))*i)*DEGTORAD;// control pointpoints[i]=centerOfTrack+radius*b2Vec2(sin(alpha),cos(alpha));// fill in the rest of the intervalconstintstartIntervalIdx=i-m_settings.down_step;if(startIntervalIdx>=0){constintendIntervalIdx=i;lerpInterval(startIntervalIdx,endIntervalIdx,points,m_settings.track_size);}}//Make the end point be the start pointpoints[m_settings.track_size-1]=points[0];//lerp last part if track_size is not a multiple of downstepif(i>=m_settings.track_size){lerpInterval(i-m_settings.down_step,m_settings.track_size-1,points,m_settings.track_size);}//apply a simple smooth filter (moving average 3, centered)for(inti=0;i<m_settings.smooth_iterations;i++){for(intk=0;k<m_settings.track_size;k++){// wrap around indexintprev_idx=((k-1)<0)?(m_settings.track_size-1):(k-1);intnext_idx=((k+1)>=m_settings.track_size)?0:(k+1);// moving averagepoints[k]=1/3.f*(points[prev_idx]+points[k]+points[next_idx]);}}}voidCTrack::lerpInterval(intstartIdx,intendIdx,b2Vec2*points,intsize){constintlenInterval=endIdx-startIdx;for(intj=startIdx;j<endIdx&&j<size;j++){constfloatammount=1.0f-(endIdx-j)/(float)m_settings.down_step;points[j]=Lerp(points[startIdx],points[endIdx],ammount);}}
After generating the center line, I map it to my logic representation of the track and then generate the static physic objects. The main function that generates the track looks like this:
As you may have noticed, I recently started using TOML files for storing config stuff (and actually even some physic shapes like the car body). The reason for this is that I kind of got tired of XML and JSON and found this format to be really nice and human readable. Of course it’s not as fast as binary files but it beats writing an editor :) . Carvatar actually has a modified version of the TOML parser that compiles on windows (small modifications made by me)
I’ll put the TOML file parse code here so you can see why I like it (did not do any data validation :D ):