AI triggers scenario ending?

Discussion in 'Programming' started by danielr, Sep 7, 2019.

  1. danielr

    danielr
    Expand Collapse

    Joined:
    Jan 6, 2014
    Messages:
    81
    I'd like to keep a race scenario running until the player reaches the final waypoint, no matter if the AI arrived there first. The Field run scenario works like this as the AI can arrive at the final waypoint while the player can still drive.

    Using some of the lua functions from (Field run) I added variables for lap racing for multiple opponents. Unfortunately the AI still triggers the scenario ending if they arrive at their waypoint before the player.
    Is there something wrong in the logic/lua amd why do AI cars trigger the scenario end? The function onBeamNGTrigger(data) sould only set AI to aiFinished = true, for the function onRaceWaypointReached(data) to check if players arrives beofer or after the AI to trigger the success messages.

    Here are the main relevant lua functions:
    Code:
    local function onRaceWaypointReached(data)
      --log('I', logTag,'onRaceWaypointReached called ')
      --dump(data)
    
      -- The player wins if he crosses the final waypoint while still being close enough to the AI (distance implied by the scenario failing if AI finishes far enough from player)
      if data.vehicleName == playerInstance and data.waypointName == finalWaypointName then
        lapsCompleted[data.vehicleName] = lapsCompleted[data.vehicleName] + 1
        dump(data)
        if lapsCompleted['scenario_player0'] == noOfLaps then
        if aiFinished then
          dump(data)
          playerWon = true
        else --Display special message if player reaches final waypoint before AI
          playerSpecialWin = true
        end
          onRaceResult()
        end
      end
    end

    Code:
    local function onBeamNGTrigger(data)
      if data.triggerName == finalTriggerName and data.event == 'enter' then
        if data.subjectName == aiInstance1 or data.subjectName == aiInstance2 or data.subjectName == aiInstance3 or data.subjectName == aiInstance4 or data.subjectName == aiInstance5 or data.subjectName == aiInstance6 or data.subjectName == aiInstance7 or data.subjectName == aiInstance8 or data.subjectName == aiInstance9 then
    
          lapsCompleted[data.subjectName] = lapsCompleted[data.subjectName] + 1
          dump(data)
          if lapsCompleted[data.subjectName] == noOfLaps then
            dump(data)
            scenetree.findObject(data.subjectName):queueLuaCommand('ai.stopFollowing()')
            aiFinished = true
          end
        end
      end
    end

    Code:
    local M = {}
    
    local helper = require('scenario/scenariohelper')
    local logTag = 'hirochiF8races94'
    
    local finalWaypointName = 'derby_wp14'
    local finalTriggerName = 'trigger_finish1'
    local playerInstance = 'scenario_player0'
    local aiInstance1 = 'scenario_opponent1'
    local aiInstance2 = 'scenario_opponent2'
    local aiInstance3 = 'scenario_opponent3'
    local aiInstance4 = 'scenario_opponent4'
    local aiInstance5 = 'scenario_opponent5'
    local aiInstance6 = 'scenario_opponent6'
    local aiInstance7 = 'scenario_opponent7'
    local aiInstance8 = 'scenario_opponent8'
    local aiInstance9 = 'scenario_opponent9'
    local playerWon = false
    local playerSpecialWin = false
    local aiFinished = false
    local running = false
    local noOfLaps = 2
    local lapsCompleted = {}
    
    local function reset()
      lapsCompleted['scenario_player0'] = 0
      lapsCompleted['scenario_opponent1'] = 0
      lapsCompleted['scenario_opponent2'] = 0
      lapsCompleted['scenario_opponent3'] = 0
      lapsCompleted['scenario_opponent4'] = 0
      lapsCompleted['scenario_opponent5'] = 0
      lapsCompleted['scenario_opponent6'] = 0
      lapsCompleted['scenario_opponent7'] = 0
      lapsCompleted['scenario_opponent8'] = 0
      lapsCompleted['scenario_opponent9'] = 0
     
      running = false
      playerWon = false
      playerSpecialWin = false
      aiFinished = false
    end
    
    local function fail(reason)
      scenario_scenarios.finish({failed = reason})
      reset()
    end
    
    local function success(reason)
      scenario_scenarios.finish({msg = reason})
      reset()
    end
    
    local racers = {"scenario_opponent1", "scenario_opponent2", "scenario_opponent3", "scenario_opponent4", "scenario_opponent5", "scenario_opponent6", "scenario_opponent7", "scenario_opponent8", "scenario_opponent9"}
    local filenames = {"f8mc-1sunburstofforad", "f8mc-2pickupmarauder", "f8mc-3etki300m", "f8mc-4legranluxev6", "f8mc-5fullsizedriftmissile", "f8mc-6pessimazxblue", "f8mc-7coupe", "f8mc-8covetgreenrally", "f8mc-9bluebucklila"}
    
    local function onRaceResult(outcome)
      dump(outcome)
      scenetree.findObject(aiInstance1):queueLuaCommand('ai.stopFollowing()')
      if playerWon then
        success('scenarios.italy.crestRoadAutobello.win.msg')
      end
      if playerSpecialWin then
        success('scenarios.italy.crestRoadAutobello.win.overtake.msg')
      end
    end
    
    local function onCountdownStarted()
      reset()
    
      -- Start the ai
      local recordings = {}
        for i, v in ipairs(racers) do
            local id = scenetree.findObject(racers[i]):getId()
            local filename = "/replays/scriptai/tracks/"..filenames[i]..".track.json"
            local scriptData = jsonReadFile(filename)
            --dump(scriptData)
            recordings[id] = scriptData.recording
            scenetree.findObject(racers[i]):queueLuaCommand("ai.startFollowing("..serialize(recordings[id])..")")
        end
    end
    
    local function onRaceStart()
      -- log('I', logTag,'onRaceStart called')
      running = true
    
    end
    
    
    local function onRaceWaypointReached(data)
      --log('I', logTag,'onRaceWaypointReached called ')
      --dump(data)
    
      -- The player wins if he crosses the final waypoint while still being close enough to the AI (distance implied by the scenario failing if AI finishes far enough from player)
      if data.vehicleName == playerInstance and data.waypointName == finalWaypointName then
        lapsCompleted[data.vehicleName] = lapsCompleted[data.vehicleName] + 1
        dump(data)
        if lapsCompleted['scenario_player0'] == noOfLaps then
        if aiFinished then
          dump(data)
          playerWon = true
        else --Display special message if player reaches final waypoint before AI
          playerSpecialWin = true
        end
          onRaceResult()
        end
      end
    end
    
    local function onBeamNGTrigger(data)
      if data.triggerName == finalTriggerName and data.event == 'enter' then
        if data.subjectName == aiInstance1 or data.subjectName == aiInstance2 or data.subjectName == aiInstance3 or data.subjectName == aiInstance4 or data.subjectName == aiInstance5 or data.subjectName == aiInstance6 or data.subjectName == aiInstance7 or data.subjectName == aiInstance8 or data.subjectName == aiInstance9 then
    
          lapsCompleted[data.subjectName] = lapsCompleted[data.subjectName] + 1
          dump(data)
          if lapsCompleted[data.subjectName] == noOfLaps then
            dump(data)
            scenetree.findObject(data.subjectName):queueLuaCommand('ai.stopFollowing()')
            aiFinished = true
          end
        end
      end
    end
    
    M.onCountdownStarted = onCountdownStarted
    M.onRaceStart = onRaceStart
    M.onRaceWaypointReached = onRaceWaypointReached
    M.onBeamNGTrigger = onBeamNGTrigger
    M.onRaceResult = onRaceResult
    return M

    Code:
    local M = {}
    
    local helper = require('scenario/scenariohelper')
    local logTag = 'crestRoadAutobello'
    
    local finalWaypointName = 'scenario_wpEnd'
    local finalTriggerName = 'finishTrigger'
    local playerInstance = 'scenario_player0'
    local aiInstance = 'scenario_ai0'
    local playerWon = false
    local playerSpecialWin = false
    local aiWon = false
    local aiFinished = false
    local damageFail = false
    local running = false
    
    local distance = 0
    local playerDamage = 0
    local aiDamage = 0
    
    local messageText = ""
    local lastMessage = ""
    
    local function reset()
      running = false
      playerWon = false
      playerSpecialWin = false
      aiWon = false
      damageFail = false
      aiFinished = false
      playerDamage = 0
      aiDamage = 0
      distance = 0
      lastMessage = ""
    end
    
    local function fail(reason)
      scenario_scenarios.finish({failed = reason})
      reset()
    end
    
    local function success(reason)
      scenario_scenarios.finish({msg = reason})
      reset()
    end
    
    local function onRaceResult(outcome)
    
      scenetree.findObject(aiInstance):queueLuaCommand('ai.stopFollowing()')
    
      if aiWon then
        fail('scenarios.italy.crestRoadAutobello.fail.msg')
      end
      if damageFail then
        fail('scenarios.italy.crestRoadAutobello.damage.msg')
      end
      if playerWon then
        success('scenarios.italy.crestRoadAutobello.win.msg')
      end
      if playerSpecialWin then
        success('scenarios.italy.crestRoadAutobello.win.overtake.msg')
      end
    end
    
    local function onCountdownStarted()
      reset()
    
      -- Start the ai
      local aiData = jsonReadFile('levels/italy/scenarios/crestRoadAutobello.track.json')
      scenetree.findObject(aiInstance):queueLuaCommand("ai.startFollowing("..serialize(aiData.recording)..")")
    end
    
    local function onRaceStart()
      -- log('I', logTag,'onRaceStart called')
      running = true
    
    end
    
    local function onPreRender(dt)
    
      -- Gets the distance between the 2 cars
      if running == true then
        local playerVehicle = scenetree.findObject(playerInstance)
        local playerVehicleData = map.objects[playerVehicle:getID()]
        playerDamage = playerVehicleData.damage
        local playerVehiclePos = playerVehicle:getPosition()
    
        local aiVehicle = scenetree.findObject(aiInstance)
        local aiVehicleData = map.objects[aiVehicle:getID()]
        aiDamage = aiVehicleData.damage
        local aiVehiclePos = aiVehicle:getPosition()
        distance = (playerVehiclePos - aiVehiclePos):len()
      end
    
      -- Fail scenario if player or ai is damaged
      if playerDamage > 10000 or aiDamage > 10000 then
        onRaceResult()
        damageFail = true
      end
    
      -- Warn player if he's too far behind the ai
      if distance > 50 then
        messageText = "scenarios.italy.crestRoadAutobello.far.msg"
      else
        messageText = ""
      end
    
      if messageText ~= lastMessage then
        helper.realTimeUiDisplay(messageText)
        lastMessage = messageText
      end
    end
    
    local function onRaceWaypointReached(data)
      --log('I', logTag,'onRaceWaypointReached called ')
      --dump(data)
    
      -- The player wins if he crosses the final waypoint while still being close enough to the AI (distance implied by the scenario failing if AI finishes far enough from player)
      if data.vehicleName == playerInstance and data.waypointName == finalWaypointName then
        if aiFinished then
        playerWon = true
      else --Display special message if player reaches final waypoint before AI
        playerSpecialWin = true
      end
        onRaceResult()
      end
    end
    
    local function onBeamNGTrigger(data)
      --log('I', logTag,'onBeamNGTrigger called ')
      --dump(data)
    
      if data.subjectName == aiInstance and data.triggerName == finalTriggerName and data.event == 'enter' then
        aiFinished = true
      if distance > 50 then -- The player loses if the AI reaches the end of the scenario and is far enough from the player
          aiWon = true
          onRaceResult()
      end
      end
    end
    
    M.onCountdownStarted = onCountdownStarted
    M.onRaceStart = onRaceStart
    M.onPreRender = onPreRender
    M.onRaceWaypointReached = onRaceWaypointReached
    M.onBeamNGTrigger = onBeamNGTrigger
    M.onRaceResult = onRaceResult
    return M
     
    #1 danielr, Sep 7, 2019
    Last edited: Sep 7, 2019
  2. Gamergull

    Gamergull
    Expand Collapse
    BeamNG Team

    Joined:
    Jun 3, 2018
    Messages:
    460
    Hey, sorry for the late reply. I spent some time figuring this out and typing this long reply, as it wasn't as easy as I thought. This is worth the trouble, though. There are several steps.

    Edited to show correct solution.

    Next, in your .prefab file, set "isAIControlled" to "0" for all AI vehicles.

    Code:
    new BeamNGVehicle(scenario_opponent1) {
      JBeam = "sunburst";
      color = "0.729412019 0.101961002 0.0588234998 1.13199997";
      colorPalette0 = "0.301083654 0.728916347 0.353139341 0.890999973";
      colorPalette1 = "0.0974876434 0.185390681 0.224512354 1.704";
      partConfig = "levels/hirochi_raceway/scenarios/1sun.pc";
      renderDistance = "500";
      renderFade = "0.100000001";
      datablock = "default_vehicle";
      position = "165.180527 -6.35728979 20.3994751";
      scale = "1 1 1";
      rotationMatrix = "0.618084133 -0.776483297 -0.12266434 0.779909849 0.625258744 -0.0281519536 0.0985564366 -0.0782668442 0.992048919";
      canSave = "1";
      canSaveDynamicFields = "1";
      persistentId = "5172466a-9291-4424-9b64-ea33b83f1ad3";
      mode = "Ignore";
      annotation = "CAR";
      cloneOrigin = "scenario_player0";
      isAIControlled = "0";
      licenseFormats = "[\"30-15\"]";
      licenseText = "SUNKISS";
    };
    Now, your .lua file should run things as intended. Looking at your code, everything should hopefully work. Here's something cool you can try:

    Code:
    local aiFinishCount = 0
    local finishRanks = {'1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th'}
    
    local function onRaceWaypointReached(data)
      if data.waypointName == finalWaypointName then
        lapsCompleted[data.vehicleName] = lapsCompleted[data.vehicleName] + 1
        if lapsCompleted[data.vehicleName] == noOfLaps then
          if not aiFinished then
            playerWon = true
          end
        end
      end
    end
    
    local function onBeamNGTrigger(data)
      if data.triggerName == finalTriggerName and data.event == 'enter' then
        if data.subjectName ~= playerInstance then
          lapsCompleted[data.subjectName] = lapsCompleted[data.subjectName] + 1
          if lapsCompleted[data.subjectName] == noOfLaps then
            aiFinished = true
            aiFinishCount = aiFinishCount + 1
          end
        end
      end
    end
    
    local function onRaceResult()
      local text = 'You finished the race in '..finishRanks[aiFinishCount + 1]..' place. '
      if playerWon then
        text = text..'Congratulations!'
        success(text)
      else
        fail(text)
      end
    end
    Now the scenario can tell the player which place it ended up in when they hit the finish line. I didn't test this, but it should work. Just remember to put the local variables from this excerpt at the top of your .lua file, and perhaps add aiFinishCount = 0 to the reset() function.
     
    #2 Gamergull, Sep 9, 2019
    Last edited: Sep 13, 2019
    • Informative Informative x 1
  3. danielr

    danielr
    Expand Collapse

    Joined:
    Jan 6, 2014
    Messages:
    81
    Wow, first of all thanks for the detailed help, you're a legend Gamergull and no worries, my main approach at beamng modding is calm and steady. ;)

    Edited since solution is above. Set "isAIControlled" to "0" for all AI vehicles in prefab. Old message:

    I've edited all the files and omg the ranking seems to be working :oops: but the AI is still triggering the scenario end. :confused: Also, removing the other vehicles field in the json will make the AI trigger each waypoint in the race, making the game spam " ding ding ding" sounds when 3 cars pass a wp. The active waypoint shown also cylces through everytime an AI car passes a waypoint. However, laps still get counted correctly for player and AI.

    Edit: Funnily when trying a few things, removing the original success/fail functions (I thought they might mess with the new ranking code and message) that were in the lua, the scenario doesn't stop at all. The cam freezes at the point the first AI car enters and the scenario continues to run. Changing cam isn't possible anymore.

    If you'd like to have a look and test it at some point, here's packed a zip (for easy editing it's the folder for unpacked mods) just a single scenario with 2 laps for quicker testing (it's still called 9 AI / 4 laps in scenario screen). Also, same filenames (might overwrite?) as my mod, in case you still have those files.
     

    Attached Files:

    #3 danielr, Sep 10, 2019
    Last edited: Sep 14, 2019
  4. Gamergull

    Gamergull
    Expand Collapse
    BeamNG Team

    Joined:
    Jun 3, 2018
    Messages:
    460
    Nice. Haha, I really should've tested things; I seem to have made some misconceptions in my post. I was thinking that removing the "*" was it, because the Field Run scenario didn't have it in the .json (it actually doesn't have a vehicle table there at all, thus relying on a default player name and settings). Gonna make some corrections in a while, with a bit of testing :) .
     
    • Like Like x 1
  5. Gamergull

    Gamergull
    Expand Collapse
    BeamNG Team

    Joined:
    Jun 3, 2018
    Messages:
    460
    It turns out, the key to having scenario waypoints untriggerable by AI is to set the "isAIControlled" value in the prefab to "0" for all AI vehicles. This means that onRaceWaypointReached(data) will only trigger for the player vehicle, so keep that in mind for restructuring the .lua. This is where onBeamNGTrigger(data) becomes useful; track the AI laps and wins with that instead (btw, don't forget M.onBeamNGTrigger = onBeamNGTrigger at the bottom of your .lua file).

    Short post because I'm short on time; I'll elaborate on this later.
     
    • Like Like x 1
  6. danielr

    danielr
    Expand Collapse

    Joined:
    Jan 6, 2014
    Messages:
    81
    I've put in the changes you've mentioned, asterisk back and OMG it's working. Thank you so much! :) This is probably helpful to anyone wanting to create realistic race scenarios with multiple AI.

    Being right there in the json, setting "isAIControlled" to 0 never crossed my mind. I've spent a few hours last weekend tinkering with the lua and already had the same idea that onRaceWaypointReached(data) would track the player, and onBeamNGTrigger the AI. I also had cool stuff already working like AI making a powerslide after crossing the finish line, and flashing messages for which AI car did so. But the early triggering of the scenario end was driving me a little crazy and it's funny now seeing how close it was to working as intended.

    Now I gotta think about if finishing 3rd for example is a fail or not ;)

    20190912213304_1.jpg 20190912213312_1.jpg
     
  7. Gamergull

    Gamergull
    Expand Collapse
    BeamNG Team

    Joined:
    Jun 3, 2018
    Messages:
    460
    Yay, glad it worked! I tested it out so I wouldn't be wrong, haha. But yeah, for the race rankings, I don't think I've even seen this before in any race scenarios (unless I missed something). It opens up some fun possibilities.
     
  8. umustbeloggedintododat

    umustbeloggedintododat
    Expand Collapse

    Joined:
    Feb 16, 2019
    Messages:
    1,381
    4th
     
  9. danielr

    danielr
    Expand Collapse

    Joined:
    Jan 6, 2014
    Messages:
    81
    Indeed, something new to experiment with in scenarios. Also the recent updates to rendering make races, especially with larger grids, more possible now.

    Btw a small issue I've found doing further tests, if the player and AI have a photo finish, the aifinishedcount will still count up until the end message is shown (there's a 3s delay between finish and message shown). So if 3 cars enter the finish right behind the player it'll still counts them. I think this will be easily fixed when adding another if to onBeamNGTrigger, to only count up aifinishedcount while player is not yet finished. I'll be on it this weekend, also adding back the flashing messages etc.

    Yeah something like this, 1st = gold, 2nd = silver, 3rd = bronze and everything below gets the player a wood medal. :eek:
     
  10. Gamergull

    Gamergull
    Expand Collapse
    BeamNG Team

    Joined:
    Jun 3, 2018
    Messages:
    460
    Hmm... one solution is to set running to false when the player hits the last waypoint, and test for running for the AI trigger, so that it won't count after the player finishes.

    As for medals, they're usually score based for a lot of scenarios, but I think there's a way to make them reflect your ranking. Hmm.
     
  11. danielr

    danielr
    Expand Collapse

    Joined:
    Jan 6, 2014
    Messages:
    81
    I've added a variable called playerFinished that's sets to true if player crosses final WP:
    Code:
    if lapsCompleted[playerInstance] == noOfLaps then
          playerFinished = true
          if not aiFinished then
            playerWon = true
          end
        end
    
    Then is checked to only count up AI as long as player hasn't finished:
    Code:
    if lapsCompleted[data.subjectName] == noOfLaps and not playerFinished then
            aiFinished = true
            aiFinishCount = aiFinishCount + 1
    end
    
    Medals could be cool, I've had a look in your Hectic Delivery scenario since it features a ranking score. This is my current experiment to see how things interact, the messages seem to show up correctly (always as a success). Maybe reaching the finish-line in this scenario should always be a success :confused:;)
    Code:
    local extraText = { -- lines of text to use for scenario fail or win screens
        second = "2nd",
        third = "3rd",
        noPodium = "No podium!",
        last = "Last, lol"
    }
    
    local function onRaceResult()
      local text = 'You finished the race in '..finishRanks[aiFinishCount + 1]..' place. '
      if playerWon then
        text = text..'Congratulations!'
        scenario_scenarios.finish({msg = text})
      elseif aiFinishCount == 1 then
        scenario_scenarios.finish({msg = extraText.second..' '..text})
      elseif aiFinishCount == 2 then
        scenario_scenarios.finish({msg = extraText.third..' '..text})
      elseif aiFinishCount == 9 then
        scenario_scenarios.finish({msg = extraText.last..' '..text})
      elseif aiFinishCount ~= 1 and aiFinishCount ~= 2 and aiFinishCount ~= 9 then
        scenario_scenarios.finish({msg = extraText.noPodium..' '..text})
      end
    end
    Here's a interesting detail, probably important for photo finishes. I've set the trigger to a sphere and scaled it just like the final waypoint. Now there are 3 modes. Center, contain and overlap. Testing contain currently and it seems to trigger tight finishes correctly.
    20190914015012_1.jpg
     
    #11 danielr, Sep 14, 2019
    Last edited: Sep 14, 2019
  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