Jump to content
  • entries
    941
  • comments
    5,894
  • views
    867,782

Building Turret AI: Part 2


Josh

2,798 views

 Share

I've expanded the turret AI to include a visibility test. This is performed initially when selecting a target, and is also performed on a continuous basis to make sure the target stays in view. To avoid overloading the engine I am only performing the visibility test at most once every 500 milliseconds. This provides near-instantaneous updating while preventing the picking from becoming a bottleneck and slowing down performance.

 

Adding bullets was mostly a copy and paste job from the SoldierAI script. Because we have a self-contained tracer prefab already assembled, it is easy to reassign this prefab for use with the turret.

 

 

Still left to do:

  • The FPSGun script expects hit objects to either be animated nemies or physics objects. Some revision will be required to handle the turret objects, so that the bullets will hurt the object and also apply a force at the hit point, so that it can potentially tip over the turrets.
  • Smoke needs to emit from the turret once it is killed, to signify that it is no longer operational.
  • The script needs to detect when the turret is tipped over and deactivate the AI when this occurs.
  • The model needs to be adjusted so the pivots point in the direction I want for the gun to point at the target object.

 

My updated script is below.

--Public
Script.target = nil--enemy to shoot
Script.range = 20
Script.health=100
Script.projectilepath = "Prefabs/Projectiles/tracer.pfb"

Script.shoot1sound=""--path "Fire 1 sound" "Wav file (*.wav):wav|Sound"
Script.shoot2sound=""--path "Fire 2 sound" "Wav file (*.wav):wav|Sound"
Script.shoot3sound=""--path "Fire 3 sound" "Wav file (*.wav):wav|Sound"

--Private
Script.lastfiretime=0
Script.mode = "idle"
Script.lastsearchtime=0
Script.searchfrequency = 500
Script.firefrequency = 50

function Script:Start()

   --Load sounds
   self.sound = {}

   self.sound.shoot = {}
   if self.shoot1sound~="" then self.sound.shoot[1] = Sound:Load(self.shoot1sound) end
   if self.shoot2sound~="" then self.sound.shoot[2] = Sound:Load(self.shoot2sound) end
   if self.shoot3sound~="" then self.sound.shoot[3] = Sound:Load(self.shoot3sound) end

   self.projectile = Prefab:Load(self.projectilepath)
   if self.projectile~=nil then
       self.projectile:Hide()
   end
   self.turret = self.entity:FindChild("turret")

   --Add muzzleflash to gun
   self.muzzle = self.entity:FindChild("muzzle")
   if self.muzzle~=nil then
       local mtl=Material:Create()
       mtl:SetBlendMode(5)
       self.muzzle:SetMaterial(mtl)
       mtl:Release()
       self.muzzleflash = Sprite:Create()
       self.muzzleflash:SetSize(0.35,0.35)
       local pos=self.muzzle:GetPosition(true)
       self.muzzleflash:SetPosition(pos,true)
       self.muzzleflash:SetParent(self.muzzle,true)
       mtl = Material:Load("Materials/Effects/muzzleflash.mat")
       if mtl then
           self.muzzleflash:SetMaterial(mtl)
           mtl:Release()
           mtl=nil
       end
       local light = PointLight:Create()
       light:SetRange(5)
       light:SetColor(1,0.75,0)
       light:SetParent(self.muzzleflash,flash)
       if light.world:GetLightQuality()<2 then
           light:SetShadowMode(0)    
       end
       self.muzzleflash:Hide()
   end

end

function Script:Release()
   if self.projectile~=nil then
       self.projectile:Release()
       self.projectile = nil
   end
end

function TurretSearchHook(entity,extra)
   if entity~=extra then
       if entity.script~=nil then
           if type(entity.script.health)=="number" then
               if entity.script.health>0 then
                   local d = extra:GetDistance(entity)
                   if d<extra.script.range then
                       if extra.script.target~=nil then
                           if extra.script:GetDistance(extra.script.target)>d then
                               if extra.script:GetTargetVisible(entity.script) then
                                   extra.script.target = entity.script
                               end
                           end
                       else
                           if extra.script:GetTargetVisible(entity.script) then
                               extra.script.target = entity.script
                           end
                       end
                   end
               end
           end
       end
   end
end

function Script:GetTargetVisible(target)
   if target==nil then
       target = self.target
   end
   if target==nil then
       return false
   end
   local pickinfo = PickInfo()
   local p0 = self.entity:GetAABB().center
   local p1 = target.entity:GetAABB().center
   local pickmode0 = self.entity:GetPickMode()
   local pickmode1 = target.entity:GetPickMode()
   self.entity:SetPickMode(0)
   target.entity:SetPickMode(0)
   local result = not self.entity.world:Pick(p0,p1,pickinfo,0,false,Collision.LineOfSight)
   self.entity:SetPickMode(pickmode0)
   target.entity:SetPickMode(pickmode1)    
   return result
end

function Script:UpdateWorld()

   if self.health>=0 then

       local currenttime = Time:GetCurrent()

       --Stop shooting if target is dead
       if self.target~=nil then
           if self.target.health<=0 then
               self.target = nil
           end
       end

       --Search for target
       if self.target==nil then
           if currenttime - self.lastsearchtime > self.searchfrequency then
               self.lastsearchtime = currenttime
               local pos = self.entity:GetPosition(true)
               local aabb = AABB(pos - Vec3(self.range), pos + Vec3(self.range))
               self.entity.world:ForEachEntityInAABBDo(aabb,"TurretSearchHook",self.entity)
           end
       end

       --Continuous visibility test
       if self.target~=nil then
           if currenttime - self.lastsearchtime > self.searchfrequency then
               self.lastsearchtime = currenttime
               if self:GetTargetVisible(self.target)==false then
                   self.target = nil
                   return
               end
           end
       end

       if self.target~=nil then

           --Motion tracking
           if self.turret~=nil then
               local p0 = self.turret:GetPosition(true)
               local p1 = self.target.entity:GetAABB().center
               local yplane = p1 - p0
               yplane.y=0
               self.turret:AlignToVector(yplane,1,0.1 / Time:GetSpeed(),0)
           end

           --Shoot
           if self.muzzle~=nil and self.projectile~=nil then
               if currenttime - self.lastfiretime > self.firefrequency then
                   self.lastfiretime = currenttime
                   local bullet = self.projectile:Instance()

                   self.muzzleflash:Show()
                   self.muzzleflash:EmitSound(self.sound.shoot[#self.sound.shoot])
                   self.muzzleflash:SetAngle(math.random(0,360))
                   self.muzzleflashtime=currenttime

                   if bullet~=nil then
                       bullet:Show()
                       bullet:SetPosition(self.muzzle:GetPosition(true),true)
                       bullet:SetRotation(self.muzzle:GetRotation(true),true)
                       if bullet.script~=nil then
                           bullet.script.owner = self
                           if type(bullet.script.Enable)=="function" then
                               bullet.script:Enable()
                           end
                           bullet:Turn(Math:Random(-3,3),Math:Random(-3,3),0)
                       end
                   end
               end
           end

       end

   end

   self:UpdateMuzzleFlash()

end

function Script:UpdateMuzzleFlash()
   if self.muzzleflashtime then
       if Time:GetCurrent()-self.muzzleflashtime<30 then
           self.muzzleflash:Show()
       else
           self.muzzleflash:Hide()
       end
   end
end

  • Upvote 1
 Share

5 Comments


Recommended Comments

I question if I should give thoughts on the structure as I don't think you generally care about that but I can't help myself.

 

Those sections in the UpdateWorld() would be best to be their own functions for readability segregation of duties. Also the massive nesting you have would be a lot easier to read if they were inverted pre-conditions at the top of the function. ie:

 

if entity == extra then return end

if entity.script == nil then return end

etc...

 

The conditions are much more self contained vs building on each other and also trying to match up end's with if's if someone was to edit the code later.

 

You break out UpdateMuzzleFlash() but not search for target? Why is UpdateMuzzleFlash() special to get it's own function but not search for target?

 

Again, these are just readability and maintainability tips that can make things easier on the users/and even you, when we start diving into these scripts.

  • Upvote 1
Link to comment

Sorry for the question in this old post 

this script search every near entity is this right? 

If i use this on my mortar then it shoots also the merc soldier and not only enemy (player) so i have to change the „turretSearchHook“ is this right?

Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...