AI lua file - How to break the single lane behavior

Discussion in 'Programming' started by Peregrinus, Oct 21, 2024.

  1. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    Hi there folks....

    Update 0.33 came, with a great new tool for road building and proper lane setup, but... the AI file seems to have been updated as well. Anyway...
    I'm glad that the game is getting better 1 update at a time.

    In the version 0.32, I managed to break the single lane behavior described in the title, however, as I'm no developer (hobbyist only), I made the crucial mistake of not saving the f file, so when the game updated, it overwrote all the changes..... I'm so dumb lol. Quick note... I didn't fix traffic or anything like that, I just did a "race like" AI, the best I could... so... I can't do it anymore alone and would like some help, maybe this can lead to implementation on version 0.5 maybe ?? lol

    Anyway... my findings (this entire thread is dedicated to the file ai.lua found in the lua/vehicle gaming folder:

    1 - function planAhead() is the key. For every lane information found, it is averaging the limits of Left and Right lanes, bringing the cars to the center of each Lane.
    2 - still within the planAhead() function, the section below used to be what I changed, as lane boundaries weren't nullified when driveInLane is off on version 0.32. I believe that the new tool for road creation has impacted in it somehow (I like the tool).
    Code:
            local rangeLeft = (n1.rangeLeft + n2.rangeLeft) * 0.5 -- lane range left boundary lateral coordinate in [0, 1]. 0 is left road boundary: always 0 when driveInLane is off
            local rangeRight = (n1.rangeRight + n2.rangeRight) * 0.5 -- lane range right boundary lateral coordinate in [0, 1]. 1 is right road boundary: always 1 when driveInLane is off
            local rangeLaneCount = (n1.rangeLaneCount + n2.rangeLaneCount) * 0.5 -- number of lanes in the range: always 1 when driveInLane is off
    
            local laneWidth = (rangeRight - rangeLeft) / rangeLaneCount -- self explanatory: entire width of the road when driveInLane is off i.e. 1
            local rangeBestLane = (n1.rangeBestLane + n2.rangeBestLane) * 0.5 -- best lane in the range: only one lane to pick from when driveInLane is off
    
            local laneLimLeft = linearScale(rangeLeft + rangeBestLane * laneWidth, 0, 1, -roadHalfWidth, roadHalfWidth) -- lateral coordinate of left boundary of lane rescaled to the road half width
            local laneLimRight = linearScale(rangeLeft + (rangeBestLane + 1) * laneWidth, 0, 1, -roadHalfWidth, roadHalfWidth) -- lateral coordinate of right boundary of lane rescaled to the road half width
    
    On the snippet above, I was changing the average (x0.5) to bigger arbitrary values and it was allowing the cars to skew away from the center of the lane. Now it is not working anymore.

    There's also this part that is considering half the size of the car for each side to define the center of the lane (the cross between each lateral and normal, but correct me if I'm wrong please), meaning, center the car in lane.
    Code:
              latXnorm = aiPos:xnormOnLine(posOrig, posOrig + normal)
              laneLimLeft = latXnorm - ai.width * 0.5 -- TODO: rethink the limits here
              laneLimRight = latXnorm + ai.width * 0.5
    
    Has anyone else, tried to tackle the centering of the car in lanes?
    My goal?
    Create a race mode for AIs that ignore lanes and consider the entire size of the road as it's "lane". The car can go anywhere within the "lane" while still identifying traffic and other obstacles. If they are fast and have enough momentum, they will try to overtake other drivers (ps: this happened in a race of mine)

    Please share with me any ideas of how to attain the above, I'm in the dark as of now.

    Btw, it has literally just come to mind that I should look for the logic behind the Flee mode, I will soon.

    1 thing that is still working very well for the AIs and that I can still change and now, save:
    1 - On the function calculateTarget(), right at the beginning of it there's the following:
    Code:
    local targetLength = max(aiSpeed * parameters.lookAheadKv, 4.5)
    
    This part is crucial for identification of other cars IN FRONT of each AI, so I reduced the arbitrary value of 4.5 to 0.5 to assist the AIs when they are in a very slow speed (starting grid and after slow curves)
    Code:
    local targetLength = max(aiSpeed * parameters.lookAheadKv, 0.5)
    

    PS: I know that changing original gaming files is a stupid idea and should be avoid at all costs. I keep a backup of the original file just in case I break the game. I discovered BeamNG rather late, and I like it a lot and I would like to somehow take part in its development, so... I'll keep breaking it until I find a solution.

    Word of caution for non-devs: if you don't know how to revert back, don't change anything.

    Thank you!
     
  2. r3eckon

    r3eckon
    Expand Collapse

    Joined:
    Jun 15, 2013
    Messages:
    594
    Is there a reason why you can't just turn off driveInLane to get that effect? I know you mention the setting as if it doesn't do what you want it to do but personally I use the driveUsingPath function to start races and the AI ignores lanes if I set driveInLane to "off" in the args. Still works in 0.33 and afaik it worked in 0.32 but I can't remember. Even better, that driveUsingPath function allows you to turn on a race mode for the AI that helps the AI overtake other cars without braking too hard. I think it does pretty much what you did with your modifications regarding distance to a car in front. You just need to pass specific parameters to enter this branch:

    Code:
    if noOfLaps > 1 and wpList[2] and wpList[1] == wpList[#wpList] then
          race = true
    end
    
    So more than 1 laps, at least 2 waypoints and the first waypoint should be the same as the first waypoint. You can trick the AI into entering race mode for non looping paths and just stop the AI once it races the actual last waypoint in your race.

    Just a tip for this part: use the userfolder. Basically, the game uses a "virtual" lua filesystem and everything inside the userfolder takes priority over stuff in the game folder. So if you go in the userfolder and create a file at the same relative path as a file in the game folder, the copy in the userfolder has priority and will be the one that ends up being loaded. So you should copy your modified script in the userfolder inside lua/vehicle/ before restoring the original script. If you want to be sure it loaded your modified script just add a print("yep it works") in a function you can call from console.

    This way you don't have to worry about keeping a backup of the original game script in case you break things (which you can still restore using steam by verifying game files) and you don't have to worry about game updates overwriting your work while you're sleeping. Also, if you intend on reporting bugs, it makes it so safe mode always uses original files, so you don't accidentally report a bug caused by your modifications.

    Keep in mind this file priority thing has some other considerations, such as zip files. For example the zip files in the game folder/content/vehicles/, the relative location in the userfolder isn't content/vehicles/ because for zip files (or maybe just certain zip files I'm not sure) the folder structure that's inside the zip file is what's important. So the files are basically unzipped at their respective root folders (either game folder or userfolder) when the game starts. Same thing applies to mod zip files. The final file loading order for the game goes something like this: game folder > userfolder > mods. And mods follow alphabetical loading order, starting from the repo subfolder. This is probably not something you need to worry about but still good to know in case you plan on making mods and can't figure out why your changes don't seem to load when zipped. Probably it's because another mod with a later loading order changes the same files you're trying to change.
     
  3. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    I turn it off indeed.
    When you turn the flag off, it considers the entirety of the road as a single lane, so the car has to drive within a "single" lane.


    I also use the function `driveUsingPath()`, but I honestly didn't think trying to fake a race could trigger the game in thinking differently. I was testing using a single lap only and focusing on the start of the race, just because I find the most interesting part, with all the cars trying to pass each other and so.... kind of chaotic, but fun. I'll start to test with 1+ laps.

    I didn't know this and I thank you for sharing. It will help me to keep the changes when game updates. I was very scared to update the game and then... when I did, I forgot to save my changes...




    On another note, I would like to share some of the findings that I made regarding the issues I presented.

    I like to use the parameter "avoidCars == 'on'" so all the AI avoid to hit each other constantly. They even try to carve new paths once in a while. However, the main source of all my frustration went away when I turned this parameter off (and half the cars couldn't pass the first 4 turns without breaking themselves on each other and ending their races)

    There is a change in perception of other vehicles by the AI that when avoidCars == 'on', they just stop and wait until it clears, or the cars in front of it moves.

    Code:
          if avoidCars == 'on' or v.targetType == 'follow' then
            v.length = obj:getObjectInitialLength(plID) + 0.05
            v.width = obj:getObjectInitialWidth(plID)
            local posFront = obj:getObjectFrontPosition(plID)
            local dirVec = v.dirVec
            v.posFront = dirVec * 0.3 + posFront -- I assume it is here is my issue
            v.posRear = dirVec * (-v.length) + posFront
            v.posMiddle = (v.posFront + v.posRear) * 0.5
    
            table.insert(trafficTable, v)
            trafficTableLen = trafficTableLen + 1
          end
    
    The code above is the original and my assumption is that the directional Vector for the positioning in the Front is what is causing them to "stop", however, when I tried changing those values arbitrarily to force different behaviors, I failed to collect anything.

    Changes that I have made which made improvements, in my opinion and use case, are:
    Code:
    local function calculateTarget(plan)
      aiPosOnPlan(plan)
      -- ORIGINAL -- local targetLength = max(aiSpeed * parameters.lookAheadKv, 4.5)
      local targetLength = max(aiSpeed * parameters.lookAheadKv, parameters.lookAheadKv)
    
    This changes how far the AI calculates the target for when they are slow. This has a good effect on the start of the races as they avoid hiting cars even when avoidCars is off by planning a different path when necessary.
    --- Post updated ---
    PS: I hit send accidently after pressing 'Tab' and then 'space' before finishing. I thought I was on my sublime3 editor lol
    --- Post updated ---
    Within the planAhead() function, I made the following changes:
    Code:
     
           -- local lateralXnorm = pos:xnormOnLine(posOrig, limPos) * roadHalfWidth -- [-r, r]
            local lateralXnorm = pos:xnormOnLine(posOrig, limPos)
    
    -- With the snippet above, I'm trying to limit the crossing between laterals and the normal to average itself, or basically, find the center of the road.
    -- I will confess that the impact seems minimal and the changes below are what is actually driving my cars to behave slightly different.
    
       if dispLeft > 0 or dispRight > 0 then
          local sideDisp = sqrt(dispLeft) - sqrt(dispRight)
          -- ORIGINAL -- sideDisp = min(dt * parameters.awarenessForceCoef * 10, abs(sideDisp)) * sign2(sideDisp)      
          sideDisp = min(dt * parameters.awarenessForceCoef * 10 * 0.5, abs(sideDisp)) * sign2(sideDisp)
    
    -- With the snippet above, I'm trying to reduce the side displacement between the cars as when avoidCars is ON, if cars are side by side, one of them will stop for the other to pass.
    
      -- ORIGINAL -- local aiWidthMargin = ai.width * 1 -- TODO
        local aiWidthMargin = ai.width * 0.05 + parameters.edgeDist -- TODO
    
    -- it has a TODO flag from the devs... still under their consideration too
    -- my perception after reducing the margin is that it allowed the cars to "dance" within the lane, meaning their roadLimit increase.
    -- I also added the parameter.edgeDist as I have trigger in each track that assigns a random negative value for each driver that passes through it
    -- local edgeDist = math.random(0, 5)/100 * -1;
    
    
    So, as I want to keep the championship moving on, I'll keep my changes, but set my avoidCars to OFF.

    Thank you r3ckon for your input... now I won't have to worry about losing my changes.
     
    • Like Like x 1
  4. r3eckon

    r3eckon
    Expand Collapse

    Joined:
    Jun 15, 2013
    Messages:
    594
    Oh okay so you basically want the AI to drive without trying to get in the center of the available space? My bad, I thought you only wanted to get rid of the proper traffic lane behavior. Because in my races I see the AI ignoring lane behavior but you're correct in assuming it sees the entire road as a single lane and thus still sticks to the middle of the available space most of the time. Personally that doesn't bother me because it'll still try to follow a somewhat decent racing line by sticking to the opposite side of a turn but when it's a long straight it does end up in the middle. But with enough turns and opponents that means the AI isn't constantly in the middle of the road. If the path is a straight line and the AI cars all have the same performance I guess they could easily end up in a line formation all driving the exact same path. In that case I guess it's necessary to modify the script like you're doing to get more chaotic/random races.
     
  5. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    I want the AI to calculate best route, which it does already, but without considering the center as its first viable option.

    I managed to reach again that behavior of ignoring lanes again, which is what you suggested, by setting the mode as race (just make 2+ lap circuit in the driveUsingPath() as you suggested). Now they are cutting edges on turns, looks pretty, I just need to work on the physical road limits (that new road tool is incredible for that) so they don't hit elevated walking paths.

    What keeps happening with me is that they end up in a line formation as you mentioned, even when parameters have been changed individually, that's why I'm going after the entire logic behind their behavior.

    But my final goal is to have the AI to take risks, or taking opportunities when another AI made a mistake. I was getting close to it, not optimal as it was hard to make the AI replicate, but I have 1 single example on a video, but I confess at the current moment I'm king of lazy to find it (it is on YT on the Redwood races, but as I don't know exactly which and its timestamp to show), so I'll narrate.

    In a curve, an AI tried to pass another car, and by doing so, it lost momentum when not succeeding, so the AI which was behind, passed the one who lost momentum without a snap, it just went to the side and vrum... Passed.

    I would like to see your races for reference, if you have vídeos if them somewhere, would you send me via DM?
    --- Post updated ---
    PS: in my races there's no humans. I'm not playing only the AIs alone on the track.
     
  6. r3eckon

    r3eckon
    Expand Collapse

    Joined:
    Jun 15, 2013
    Messages:
    594
    I have a few videos showing the AI but they're not very good at showing the AI behavior, also I'm always racing with them it's never purely AI:

    This one is on motorsport playground, the AI lining up actually makes sense because it's a NASCAR race. There's a few overtakes and one instance where an AI ends up pitting another car because it ended up on the inside of the turn.

    This one is on the Nordschleife, it doesn't show any interesting AI vs AI behavior but since it's a proper race track you can see how the AI does follow a decent racing line. They still clearly prefer the middle of the road when it's a straight line. Personally it doesn't bother me.

    Most of the races in my mod are 1v1s, player VS AI so there's not much interesting AI behavior going on with the exception of races with traffic enabled. In most cases the AI has a lot of trouble dealing with traffic but in certain situations it can handle it surprisingly well. There's a Brazilian youtuber that plays it and in his last video he raced against an AI that (I can only assume through sheer luck) managed to swerve around multiple traffic vehicles at full speed. I'm pretty sure this wasn't the result of AI pathing and more the fact that the AI barely had control of that car so the rear sliding around by some miracle got the AI to avoid traffic.

    Oh and this last one is a bit old by now but it shows the difference between race mode and regular AI, I purposefully brake in front of the AI to see how it reacts. It also shows some AI vs traffic behavior but as I've said before the traffic is quite hard to deal with in most situations. In some cases the AI will stop completely because traffic is blocking the entire decal road. Even though it looks like there's enough space to go around it to us human players, since the pathing doesn't look for alternate routes when there's a car completely blocking a road the AI will just stop there and get stuck.

    I'm keen on seeing what you can come up with as far as improving the AI behavior. I originally intended on making my own changes to this script but I had a bunch of other things to add to my mod back then and the race mode AI worked well enough for what I wanted so I ended up just using that instead. I also do some manual tweaking to race paths by adding brake zone waypoints. This uses the wpSpeeds parameter in driveUsingPath. You send key/value pairs of waypoints and target speed in m/s. That way I can increase the general speed limit and aggression values to the limit. With maxed out aggression and general speed limit without brake zones the AI would fly past hard 90 degree turns.
     
  7. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    I like that Brazilian video, on how the AI "costurou" other vehicles (term in Brazilian Portuguese which implies that a car "sewed" through traffic in a zig zag manner).
    For that mod, are you using the Flee mode?
    I noticed that Flee mode is the best when it comes to "sewing" through traffic, but I couldn't find any clear, and easy to understand, logic in the code that does that (I'm not a dev, so it requires me longer to actually understand what's happening in the code)

    On that Nordschleife video, I noticed the AI didn't do the "break check" when you approach from behind. Am I right to assume you race it with avoidCars OFF? Because if it was ON, and based on the car avoidance logic shared above, it would break check when you get close to it.

    For the first video, I agree that lining up makes sense for Nascar races.

    For the last video, that is actually amazing.
    Do you have a snippet on how you deliberate turn the race mode?
    I'm asking because you aren't racing on a circuit and still the mode is on.

    From my previous comment, I found my video where the AI "took advantage" of a loss of momentum by other AI. Like on the Brazilian video, this is a single and lucky event that I want the AI to replicate as often as possible.

    It starts at the moment of passing, then a replay will play from a different angle (ps: I'm learning/still improving how to set camera angles :)

    I saw the wpSpeed points and tried to use it, but I fell on a circuit micromanaging speeds that, for me, felt waste of time. My goal is to ensure that the AI makes the decision on its own regarding speed and so. But the idea is actually good.
    Probably when I have enough circuit data from previous races, I can start to limit or "set speed recommendations" for track areas.

    Also, it is also about priorities, when I'm happy with my AI, I'll start to work on other things, for more Automation of race creations, like a scenario for myself or even that flowgraph thing which I believe could simplify a lot of what I manually do.

    Keep sharing what you found, as I've learned a lot from you already.
     
    #7 Peregrinus, Oct 25, 2024
    Last edited: Oct 25, 2024
  8. r3eckon

    r3eckon
    Expand Collapse

    Joined:
    Jun 15, 2013
    Messages:
    594
    It's not flee mode, just driveUsingPath with race mode.
    I always keep avoidCars on, there is one instance in that video where the AI seems to do a brake check but it might just have gotten spooked at a turn because it's a high speed section. I actually have never experienced this on a straight line, at least I don't think I did. In the NASCAR video they get lined up bumper to bumper without brake checking and they still have the avoidance turned on. The car avoidance code probably uses vectors for this so it should be easy for the code to check if a certain car is in the same direction as the forward vector and not react to it otherwise.
    I just trick the AI into entering race mode by using 99 laps and the first waypoint is added at the end of the waypoint list regardless of what type of race it is. So as far as the AI knows, it's using a looping path with more than 1 lap. So race mode is enabled. I get the AI to stop at the end of the race by calling setMode("stop"). I keep track of current lap and checkpoints in flowgraph so that when the AI finishes the race (after completing the actual lap count, not 99 laps) flowgraph sends the stop command. While it wouldn't be a big deal for the AI to keep going on a looping path, on a drag race the AI would turn around and head back to the start.
    Yeah it is micromanaging at this point but the increase in aggression value is worth it imo. Even though the code says aggression can be set up to 1, the UI app to manage AI actually had a numeric up down control that let the value go up to 2. I think (might be wrong) that since the comment says 1 is the limit of grip, above 1 the AI will be taking corners faster than it thinks is possible. Which in most situations is actually still slow enough to take a turn without crashing, but the downside is that for 90 degree corners after fast zones the AI will just miss the turn completely unless waypoints with target speeds are used to force it to brake.
     
    #8 r3eckon, Oct 25, 2024
    Last edited: Oct 25, 2024
  9. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    Sorry for the delay, I got caught up with some other things and couldn't come back earlier.

    I'll give it a try, this is a one job, once you have the path saved on a json file, you don't need to micromanage again. I noticed that higher aggression make the drivers more interesting to watch indeed, and managing the curves feels like a nice experiment that has huge potential.

    R3eckon, there's also another parameter which defines how early/late a driver should react, is mostly visible when on a curve. It's called lookAheadKv. I just call it vision. If not, play with it as it also brings more random AI behaviors.

    I'll come back to this the read with the results of my experiment.
     
  10. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    Hi everyone!!

    After 2 months of ups and downs, I managed to make some advancements in regards to AI in BeamNG. I call it a breakthrough because I'm a selfish person that thinks this work can open the door for other more intelligent minds than mine and improve it, make it better. And who knows, get into the game itself?

    TLDR: Made the AI more aggressive and proactive in overtaking slower vehicles, even in tight spaces or single-lane scenarios by creating helper functions. Improved the AI’s driving behavior, especially on sharp corners and straights, to make it more human-like and prevent cutting through grass or swinging back to the center mid-curve by creating more helper functions. Updated a few existing functions (see further below) to ensure helper functions apply properly.

    Anyway.... the file is attached. Please use it at your own discretion, but give credit where it is due if needed.
    Installation folder is the same as MOD, but at a different level.
    >> path_to_your_game_files/lua/vehicle/ai.lua

    As an example, mine is at:
    >> C:/Users/<myUSerName>/AppData/Local/BeamNG.drive/0.34/lua/vehicle/ai.lua
    PS: you may need to create the folders 'lua' and 'vehicle'

    Keep in mind that THIS IS NOT A MOD but an attempt to improve race conditions for AI. Modders can use it if they want on their scenarios.

    Before jumping to the changes, as it will get long and boring for a few, I would like to present my observations.
    OBS: I'm no programmer/coder/hacker or whatever, I'm just a unemployed analyst who can't find job anymore but still have a curious and active mind who happens to know a bit or two on how to use tools to help achieve goals. And a bit of extra time to spare. While I reckon it is still not to MY desired goals, it may be useful to others in the community.
    I do reckon many things could potentially be improved/better applied within the code. Anyhow, I'll keep working on it.
    I also used DeepSeek v3 to help me with plenty of the functions themselves as I would never be able to accomplish this on my own. Same as a strong person already lifts heavy things, but needs a forklift to lift even heavier things. The difference is that, to operate a forklift, you need to at least know where and how to place things before starting using it.

    Thank you!



    Key changes:

    - Updated the function calculateTarget(plan) with the aim to make the target of the AI as dynamic as possible considering other cars around and curvature of a track. This function is also used in race to determine the shortest path
    Code:
    local function calculateTarget(plan)
      aiPosOnPlan(plan)
    
      -- Base target length calculation
      local baseTargetLength = max(ai.speed * parameters.lookAheadKv, 4.5)
    
      -- Adjust targetLength based on traffic density and AI's speed
      local trafficDensity = #trafficTable  -- Number of vehicles in the traffic table
      local speedFactor = clamp(ai.speed / 30, 0.5, 2)  -- Normalize speed factor between 0.5 and 2
      local trafficFactor = 1 + (trafficDensity * 0.2)  -- Increase target length if there are more vehicles nearby
    
      -- Calculate dynamic targetLength
      local targetLength = baseTargetLength * speedFactor * trafficFactor
    
      -- Ensure the target length is not too short or too long
      targetLength = clamp(targetLength, 4.5, 30)  -- Clamp between 4.5m and 30m
    
      -- Function to calculate local traffic density near a plan node
      local function getLocalTrafficDensity(nodePos, radius)
        local density = 0
        for _, vehicle in ipairs(trafficTable) do
          if vehicle.pos:distance(nodePos) <= radius then
            density = density + 1
          end
        end
        return density
      end
    
      -- Function to calculate curvature between two segments
      local function calculateCurvature(pos1, pos2, pos3)
        local vec1 = pos2 - pos1
        local vec2 = pos3 - pos2
        local cross = vec1:cross(vec2)
        local curvature = cross:length() / (vec1:length() * vec2:length() + 1e-30)  -- Avoid division by zero
        return curvature
      end
    
      local function getCurvatureFactor(curvature)
        -- Define curvature thresholds and scaling factors
        local straightThreshold = 0.05  -- Curvature below this is considered a straight
        local moderateThreshold = 0.2   -- Curvature below this is considered a moderate turn
        local sharpThreshold = 0.5      -- Curvature above this is considered a sharp turn
    
        -- Define scaling factors for each curvature range
        local straightFactor = 1.0      -- No adjustment on straights
        local moderateFactor = 5        -- Gentle adjustment on moderate turns
        local sharpFactor = 20          -- Aggressive adjustment on sharp turns
    
        -- Piecewise function to calculate curvature factor
        local factor
        if curvature < straightThreshold then
          -- Straight section: no adjustment
          factor = straightFactor
        elseif curvature < moderateThreshold then
          -- Moderate turn: gentle adjustment
          factor = 1 / (1 + curvature * moderateFactor)
        else
          -- Sharp turn: aggressive adjustment
          factor = 1 / (1 + curvature * sharpFactor)
        end
    
        -- Clamp the factor to ensure it stays within reasonable bounds
        return clamp(factor, 0.5, 1)
      end
    
      -- Adjust targetLength based on the AI's position along the current segment (if there are enough plan nodes)
      if plan.planCount >= 3 then
        local xnorm = clamp(plan.aiXnormOnSeg, 0, 1)  -- Normalized position along the current segment
        local remainingDistanceCurrentSeg = plan[1].length * (1 - xnorm)  -- Remaining distance in the current segment
        local weightedNextSegLength = plan[2].length * xnorm  -- Weighted contribution of the next segment
    
        -- Calculate curvature for the current and next segments
        local curvatureCurrent = calculateCurvature(plan[1].pos, plan[2].pos, plan[3].pos)
        local curvatureNext = calculateCurvature(plan[2].pos, plan[3].pos, plan[4] and plan[4].pos or plan[3].pos)
    
        -- Get curvature factors for current and next segments
        local curvatureFactorCurrent = getCurvatureFactor(curvatureCurrent)
        local curvatureFactorNext = getCurvatureFactor(curvatureNext)
    
        -- Apply curvature factors to remaining distance and weighted next segment length
        remainingDistanceCurrentSeg = remainingDistanceCurrentSeg * curvatureFactorCurrent
        weightedNextSegLength = weightedNextSegLength * curvatureFactorNext
    
        -- Update targetLength to account for the current and next segment
        targetLength = max(targetLength, remainingDistanceCurrentSeg, weightedNextSegLength)
    
        -- Optional: Add a buffer for smoother transitions between segments
        local transitionBuffer = 2.0  -- Add a small buffer to smooth transitions
        targetLength = targetLength + transitionBuffer
      end
    
    
    
    - created a set of helper functions to determine new variables that could be changed based on user's need. I use this to create "personalities" to my drives as they are "simulated people" with different "capabilities". Default values are applied globally to M. I'm personally unsure if this is the best way to do, open to suggestions here.
    Code:
    -- Default safety distance multiplier
    local function setSafetyDistance(v)
      if type(v) == "number" and v >= 0 then
        M.safetyDistance = v
        -- print("Safety distance set to: " .. v)
      else
        print("Invalid safety distance. Please provide a number >= 0.")
      end
      stateChanged()
    end
    
    -- Default lateral offset range (40% of track width)
    local function setLateralOffsetRange(v)
      if type(v) == "number" and v >= 0 and v <= 1 then
        M.lateralOffsetRange = v
        -- print("Lateral offset range set to: " .. v)
      else
        print("Invalid lateral offset range. Please provide a number between 0 and 1.")
      end
      stateChanged()
    end
    
    -- Default lateral offset scale for overtaking (30% of track width)
    local function setLateralOffsetScale(v)
      if type(v) == "number" and v >= 0 and v <= 1 then
        M.lateralOffsetScale = v
        -- print("Lateral offset scale set to: " .. v)
      else
        print("Invalid lateral offset scale. Please provide a number between 0 and 1.")
      end
      stateChanged()
    end
    
    -- Bias towards the shortest path (0 = no bias, 1 = always shortest path)
    local function setShortestPathBias(v)
      if type(v) == "number" and v >= 0 and v <= 1 then
        M.shortestPathBias = v
        -- print("Shortest path bias set to: " .. v)
      else
        print("Invalid shortest path bias. Please provide a number between 0 and 1.")
      end
      stateChanged()
    end
    
    -- setting default values
    M.safetyDistance = 0.5
    M.lateralOffsetRange = 0.4
    M.lateralOffsetScale = 0.3
    M.shortestPathBias = 0.7
    
    - new function used only in racingMode. This is my approach to change AI behavior without changing existing functions, but just to add on top of it, like a cherry or just a bunch of sauce. Anyway you like it :D
    This function acts as the "brain" of the AI when in raceMode. The AI will calculate best path, curvature and decide if an overtake is up for grabs. This function works ONLY if you have avoidCars == 'on'. That is my arbitrary decision because safetyDistance wouldn't make any sense, either ovetake attempts if they could just ram other cars out of the way. It also sets raceMode == "off" globally.
    Code:
    local function racingBehavior(route, baseRoute)
      if avoidCars ~= 'on' then
        print("Racing behavior is inactive because avoidCars is not 'on'.")
        return
      end
    
      local safetyDistance = M.safetyDistance
      local lateralOffsetRange = M.lateralOffsetRange
      local lateralOffsetScale = M.lateralOffsetScale
      local shortestPathBias = M.shortestPathBias
    
      -- Initialize plan if it doesn't exist
      if not route.plan then
        route.plan = {}
        print("Initialized empty plan in racingBehavior.")
      end
    
      -- Override lateral movement logic to prioritize the shortest path
      local plan = route.plan
      for i = 1, #plan do
        local n = plan[i]
        if not driveInLaneFlag then
          local trackWidth = n.radiusOrig * n.chordLength * 2
    
          -- Calculate the optimal lateral position for the shortest path
          local curvature = n.curvature or 0
          local shortestPathOffset = 0
          if curvature > 0 then
            -- On a curve, bias towards the inside (negative offset)
            shortestPathOffset = -trackWidth * 0.5 * shortestPathBias
          elseif curvature < 0 then
            -- On a curve, bias towards the outside (positive offset)
            shortestPathOffset = trackWidth * 0.5 * shortestPathBias
          end
    
          -- Add some randomness for realism and overtaking
          local randomOffset = math.random(-trackWidth * lateralOffsetRange, trackWidth * lateralOffsetRange)
          local lateralOffset = shortestPathOffset + randomOffset * (1 - shortestPathBias)
    
          -- Clamp the lateral offset to stay within track bounds
          n.lateralXnorm = clamp(n.lateralXnorm + lateralOffset, -trackWidth, trackWidth)
        end
      end
    
      -- Override avoidCars logic with configurable safety distance
      print("Number of vehicles in trafficTable: " .. #trafficTable)  -- Debug statement
      for i = 2, #plan - 1 do
        local n1, n2 = plan[i], plan[i+1]
        for _, v in ipairs(trafficTable) do
          if v.vel:dot(n1.dirVec) < ai.speed * 0.6 then
            print("Overtaking opportunity detected!")  -- Debug statement
            local side = math.random(-1, 1)
            local lateralOffset = side * (n1.radiusOrig * n1.chordLength * lateralOffsetScale)
            n1.lateralXnorm = clamp(n1.lateralXnorm + lateralOffset, -n1.radiusOrig, n1.radiusOrig)
            n1.speed = ai.speed * 1.2
            break
          end
        end
    
        -- Adjust safety distance
        local minSqDist = math.huge
        if rnorm > 0 and rnorm < 1 and vnorm > 0 and vnorm < 1 then
          minSqDist = 0
        else
          minSqDist = min(minSqDist, square((ai.width + limWidth) * safetyDistance))  -- Use configurable safety distance
        end
      end
    end
    
    -- Define raceMode as a global variable
    raceMode = 'off'  -- Default value
    
    - the main dish is to be served now. the update to function driveUsingPath(). This is the main deal as this is where all comes to life. And this is where it may not be useful to everyone else, kind of an isolated solution for my own use case which is to make AI races among themselves without using the scriptAI tool.
    this is where you would turn on the raceMode as a parameter within this function. You also apply here all the helper functions everytime you turn on an AI with path. There are some debug messages printing on console, which for me are useful, but can be disabled if you know what you're doing.
    Code:
    local function driveUsingPath(arg)
      --[[ At least one argument of either path or wpTargetList or script must be specified. All other arguments are optional.
    
      * path: A sequence of waypoint names that form a path by themselves to be followed in the order provided.
      * wpTargetList: A sequence of waypoint names to be used as succesive targets ex. wpTargetList = {'wp1', 'wp2'}.
                      Between any two consequitive waypoints a shortest path route will be followed.
      * script: A sequence of positions
    
      -- Optional Arguments --
      * wpSpeeds: Type: (key/value pairs, key: "node_name", value: speed, number in m/s)
                  Define target speeds for individual waypoints. The ai will try to meet this speed when at the given waypoint.
      * noOfLaps: Type: number. Default value: nil
                  The number of laps if the path is a loop. If not defined, the ai will just follow the succesion of waypoints once.
      * routeSpeed: A speed in m/s. To be used in tandem with "routeSpeedMode".
                    Type: number
      * routeSpeedMode: Values: 'limit': the ai will not go above the 'routeSpeed' defined by routeSpeed.
                                'set': the ai will try to always go at the speed defined by "routeSpeed".
      * driveInLane: Values: 'on' (anything else is considered off/inactive)
                     When 'on' the ai will keep on the correct side of the road on two way roads.
                     This also affects pathFinding in that when this option is active ai paths will traverse roads in the legal direction if posibble.
                     Default: inactive
      * aggression: Value: 0.3 - 1. The aggression value with which the ai will drive the route.
                    At 1 the ai will drive at the limit of traction. A value of 0.3 would be considered normal every day driving, going shopping etc.
                    Default: 0.3
      * avoidCars: Values: 'on' / 'off'.  When 'on' the ai will be aware of (avoid crashing into) other vehicles on the map. Default is 'off'
      * raceMode: Values: 'on' / 'off'. When 'on', enables custom racing behavior (e.g., lateral movement, overtaking). Default is 'off'
      * safetyDistance: Type: number. Default: 0.5
                       Controls how closely cars follow each other. Lower values allow closer following.
      * lateralOffsetRange: Type: number. Default: 0.4
                           Controls how much lateral movement is allowed within the track width (0 = no movement, 1 = full track width).
      * lateralOffsetScale: Type: number. Default: 0.3
                           Controls how aggressively cars move laterally during overtaking (0 = no overtaking, 1 = full track width).
      * shortestPathBias: Type: number. Default: 0.7
                         Controls how strongly cars prioritize the shortest path (0 = no bias, 1 = always shortest path).
      * examples:
      ai.driveUsingPath{ wpTargetList = {'wp1', 'wp10'}, driveInLane = 'on', avoidCars = 'on', routeSpeed = 35, routeSpeedMode = 'limit', wpSpeeds = {wp1 = 10, wp2 = 40}, aggression = 0.3, raceMode = 'on'}
      In the above example the speeds set for wp1 and wp2 will take precedence over "routeSpeed" for the specified nodes.
      --]]
    
      -- Validate input arguments
      if (arg.wpTargetList == nil and arg.path == nil and arg.script == nil) or
        (type(arg.wpTargetList) ~= 'table' and type(arg.path) ~= 'table' and type(arg.script) ~= 'table') or
        (arg.wpSpeeds ~= nil and type(arg.wpSpeeds) ~= 'table') or
        (arg.noOfLaps ~= nil and type(arg.noOfLaps) ~= 'number') or
        (arg.routeSpeed ~= nil and type(arg.routeSpeed) ~= 'number') or
        (arg.routeSpeedMode ~= nil and type(arg.routeSpeedMode) ~= 'string') or
        (arg.driveInLane ~= nil and type(arg.driveInLane) ~= 'string') or
        (arg.aggression ~= nil and type(arg.aggression) ~= 'number') or
        (arg.raceMode ~= nil and type(arg.raceMode) ~= 'string') or
        (arg.safetyDistance ~= nil and type(arg.safetyDistance) ~= 'number') or
        (arg.lateralOffsetRange ~= nil and type(arg.lateralOffsetRange) ~= 'number') or
        (arg.lateralOffsetScale ~= nil and type(arg.lateralOffsetScale) ~= 'number') or
        (arg.shortestPathBias ~= nil and type(arg.shortestPathBias) ~= 'number')
      then
        return
      end
    
      raceMode = arg.raceMode or 'off'  -- Default to 'off' only if not provided
     
      -- Debug statement for raceMode
      if arg.raceMode == 'on' and arg.avoidCars == 'on' then
        print("Race mode is ON. Racing behavior is active.")
    
        -- Initialize currentRoute only when raceMode is 'on'
        currentRoute = {
          path = arg.wpTargetList,  -- Use the provided waypoints
          plan = {}  -- Initialize an empty plan
        }
    
        -- Enable racing behavior
        racingBehavior(currentRoute, baseRoute)
      else
        print("Race mode is OFF. Default behavior is active.")  -- Debug statement
      end
    
      -- Update parameters in the M table
      M.safetyDistance = arg.safetyDistance
      M.lateralOffsetRange = arg.lateralOffsetRange
      M.lateralOffsetScale = arg.lateralOffsetScale
      M.shortestPathBias = arg.shortestPathBias
    
      if arg.script then
        -- Set vehicle position and orientation at the start of the path
        -- Get initial position and orientation of vehicle at start of path (possibly time offset and/or time delayed)
        local script = arg.script
        local dir, up, pos
        if script[1].dir then
          -- vehicle initial orientation vectors exist
    
          dir = vec3(script[1].dir)
          up = vec3(script[1].up or mapmgr.surfaceNormalBelow(vec3(script[1])))
    
          local frontPosRelOrig = obj:getOriginalFrontPositionRelative() -- original relative front position in the vehicle coordinate system (left, back, up)
          local vx = dir * -frontPosRelOrig.y
          local vz = up * frontPosRelOrig.z
          local vy = dir:cross(up) * -frontPosRelOrig.x
          pos = vec3(script[1]) - vx - vz - vy
          local dH = require('scriptai').wheelToGroundDist(pos, dir, up)
          pos:setAdd(dH * up)
        else
          -- vehicle initial orientation vectors don't exist
          -- estimate vehicle orientation vectors from path and ground normal
    
          local p1 = vec3(script[1])
          local p1z0 = p1:z0()
          local scriptPosi = vec3()
          local k
          for i = 2, #script do
            scriptPosi:set(script[i].x, script[i].y, 0)
            if p1z0:squaredDistance(scriptPosi) > 0.2 * 0.2 then
              k = i
              break
            end
          end
    
          if k then
            local p2 = vec3(script[k])
            dir = p2 - p1; dir:normalize()
            up = mapmgr.surfaceNormalBelow(p1)
    
            local frontPosRelOrig = obj:getOriginalFrontPositionRelative() -- original relative front position in the vehicle coordinate system (left, back, up)
            local vx = dir * -frontPosRelOrig.y
            local vz = up * frontPosRelOrig.z
            local vy = dir:cross(up) * -frontPosRelOrig.x
            pos = p1 - vx - vz - vy
            local dH = require('scriptai').wheelToGroundDist(pos, dir, up)
            pos:setAdd(dH * up)
          end
        end
    
        if dir then
          local rot = quatFromDir(dir:cross(up):cross(up), up)
          obj:queueGameEngineLua(
            "be:getObjectByID(" .. objectId .. "):resetBrokenFlexMesh();" ..
            "vehicleSetPositionRotation(" .. objectId .. "," .. pos.x .. "," .. pos.y .. "," .. pos.z .. "," .. rot.x .. "," .. rot.y .. "," .. rot.z .. "," .. rot.w .. ")"
          )
    
          mapmgr.setCustomMap() -- nils mapmgr.mapData
          M.mode = 'manual'
          stateChanged()
    
          local pathMap = require('graphpath').newGraphpath()
          local path = {}
          local radius = obj:getInitialWidth()
          local outNode, outPos
          local speedProfile = {}
          for i = 1, #arg.script - 1 do
            local inNode = 'wp_'..tostring(i)
            outNode = 'wp_'..tostring(i+1)
            local inPos = vec3(arg.script[i].x, arg.script[i].y, arg.script[i].z)
            outPos = vec3(arg.script[i+1].x, arg.script[i+1].y, arg.script[i+1].z)
            pathMap:uniEdge(inNode, outNode, inPos:distance(outPos), 1, 100, nil, false)
            pathMap:setPointPositionRadius(inNode, inPos, radius)
            table.insert(path, inNode)
            speedProfile[inNode] = arg.script[i].v
          end
          pathMap:setPointPositionRadius(outNode, outPos, radius)
          table.insert(path, 'wp_'..tostring(#arg.script))
          speedProfile[outNode] = arg.script[#arg.script].v
    
          scriptData = deepcopy(arg)
          scriptData.mapData = pathMap
          scriptData.path = path
          scriptData.speedProfile = speedProfile
        end
      else
        setState({mode = 'manual'})
    
        setParameters({
          driveStyle = arg.driveStyle or 'default',
          staticFrictionCoefMult = max(0.95, arg.staticFrictionCoefMult or 0.95),
          lookAheadKv = max(0.1, arg.lookAheadKv or parameters.lookAheadKv),
          understeerThrottleControl = arg.understeerThrottleControl,
          oversteerThrottleControl = arg.oversteerThrottleControl,
          throttleTcs = arg.throttleTcs
        })
    
        noOfLaps = arg.noOfLaps and max(arg.noOfLaps, 1) or 1
        wpList = arg.wpTargetList
        manualPath = arg.path
        validateInput = validateUserInput
        avoidCars = arg.avoidCars or 'off'
    
        if noOfLaps > 1 and wpList[2] and wpList[1] == wpList[#wpList] then
          race = true
        end
    
        speedProfile = arg.wpSpeeds or {}
        setSpeed(arg.routeSpeed)
        setSpeedMode(arg.routeSpeedMode)
        setSafetyDistance(arg.safetyDistance)
        setLateralOffsetRange(arg.lateralOffsetRange)
        setLateralOffsetScale(arg.lateralOffsetScale)
        setShortestPathBias(arg.shortestPathBias)
    
        driveInLane(arg.driveInLane)
    
        setAggressionExternal(arg.aggression)
        stateChanged()
      end
    end
    
    Thank you again!
     

    Attached Files:

    • ai.lua

      File size:
      221.7 KB
      Views:
      31
    • Like Like x 3
  11. THEARTH

    THEARTH
    Expand Collapse

    Joined:
    Jul 7, 2020
    Messages:
    44
    Is there anything in the code that does not allow the AI to drive when starting a Race from Race Editor/Tool?
     
  12. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    Not that I'm aware of as I haven't tested this option.
    Look for errors at the console.
     
  13. Nissan murano

    Nissan murano
    Expand Collapse

    Joined:
    Feb 5, 2024
    Messages:
    1,356
    I tried finding the lua folder in 0.34, but I couldn't, help?
     
  14. THEARTH

    THEARTH
    Expand Collapse

    Joined:
    Jul 7, 2020
    Messages:
    44
    --- Post updated ---
    NVM for now, I was using branched paths apparently...
     
    • Like Like x 1
  15. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    I noticed that my raceMode parameters are mandatory, but I don't provide a default within the driveUsingPath() function, breaking it.
    Later tonight I'll update it as after some more extensive tests, found some other behaviours I didn't like
     
  16. Nissan murano

    Nissan murano
    Expand Collapse

    Joined:
    Feb 5, 2024
    Messages:
    1,356
    Suggestion: Make traffic overtake you when you go slow, When you honk to much, make them chase you. Make it realistic like real life, make it like if you overtake them they flash you (+honk)
    Thanks! (You don't have to.)
     
  17. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    I know I don't have too and it sounds like a good challenge, and a nice addition to the game if accomplished.
    However, I don't think I have the skills, or the will, to do so. I don't play the game with traffic (only traffic for me that is interesting is a heavy one, buy my computer can't handle, so...). I'm left with a max of 8 cars in a racing environment. And it already scream for help.


    I also would like to announce that I create a git repository as I'm working on the file constantly.
    if you are interested, you can follow me in real time.
    Also, I'll add other versions that are working.

    I feel like I have to leave a DISCLAIMER:
    ***Make sure you know what you are doing if you decide to clone my git. It has files that may not work well or even break the game.***


    https://github.com/raulgregg/peregrinus_beamlua

    One thing I need to make clear as well, unfortunately. If a file isn't/wasn't shared by me, then it was not my creation, no matter what.
     
    #17 Peregrinus, Jan 15, 2025
    Last edited: Jan 15, 2025
  18. THEARTH

    THEARTH
    Expand Collapse

    Joined:
    Jul 7, 2020
    Messages:
    44
  19. Nissan murano

    Nissan murano
    Expand Collapse

    Joined:
    Feb 5, 2024
    Messages:
    1,356
    • Like Like x 1
  20. Peregrinus

    Peregrinus
    Expand Collapse

    Joined:
    Mar 29, 2024
    Messages:
    113
    I received an email with a question for you.

    Each iteration works they follow slightly different paths, but still work.
    However the AI has a tendency to return to a queue pattern while during a race. Each new version is an attempt to remove it.

    I also think I need to reorganise the git, as I'm not a dev, I'm open to suggestions on how to better do it.

    I'm not on my computer, but will do once I get to it.
    Could you please let us know what is your goal with this hotfix? And what I should expect from the AI?
     
  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice