NextBot NPC Oluşturma

Nextbot, Team Fortress 2 ve Left 4 Dead'deki yapay zekanın kullandığı sistemdir. Bu eğitim, düşmanları (sizi) arayacak ve ölene ya da çok uzaklaşana kadar onları kovalayacak basit bir yapay zeka için gereken adımları ele alacaktır. Ayrıca, hiç düşman olmadığında rastgele başka şeyler de yapacaktır.

Hadi başlayalım

Öncelikle, varlığınız (entity) için .lua dosyasını oluşturun. Bu eğitim için oluşturduğum dosya "addons/Nextbot_tut/lua/entities" klasöründe bulunuyor ve "simple_nextbot.lua" olarak adlandırılmıştır. Şimdi bu dosyayı açın, böylece kodu eklemeye başlayabilirsiniz.

Kod

Varlıklar için ihtiyacımız olan temel şeyler

Kullanılacak temel varlığı tanımlayarak ve onu spawn edilebilir (spawnable) yaparak başlayın. Şu ana kadar diğer tüm varlıklarla hemen hemen aynı. Burada modeli ayarlıyor ve daha sonra kullanacağımız bazı değişkenleri tanımlıyoruz.

AddCSLuaFile() ENT.Base = "base_nextbot" ENT.Spawnable = true function ENT:Initialize() self:SetModel( "models/hunter.mdl" ) self.LoseTargetDist = 2000 -- Düşmanı kaybetmeden önce ne kadar uzakta olması gerektiği self.SearchRadius = 1000 -- Ne kadar uzağa kadar düşman aranacağı end

Düşmanla ilgili şeyler

Bu bölüm, düşmanla ilgili konular için bazı kullanışlı fonksiyonlar ekler. Bir NPC/bot bir şeyleri hedefleyemiyorsa tamamlanmış sayılmaz, değil mi? Bunlar, hala bir düşman olup olmadığını ya da düşmanın kaçıp kaçmadığını kontrol eden bir fonksiyon ve düşman aramak için bir fonksiyon içerir. Ne işe yaradıklarını tam olarak bilmeniz için her türlü yorumu ekledim.

---------------------------------------------------- -- ENT:Get/SetEnemy() -- Düşmanımızı kayıtlı tutmak için kullanılan basit fonksiyonlar ---------------------------------------------------- function ENT:SetEnemy(ent) self.Enemy = ent end function ENT:GetEnemy() return self.Enemy end ---------------------------------------------------- -- ENT:HaveEnemy() -- Eğer bir düşmanımız varsa true (doğru) döndürür ---------------------------------------------------- function ENT:HaveEnemy() -- Mevcut düşmanımız geçerliyse if ( self:GetEnemy() and IsValid(self:GetEnemy()) ) then -- Düşman çok uzaktaysa if ( self:GetRangeTo(self:GetEnemy():GetPos()) > self.LoseTargetDist ) then -- Düşman kaybedildiyse, yeni bir tane aramak için FindEnemy() fonksiyonunu çağır -- FindEnemy(), bir düşman bulunursa true (doğru) döndürür, bu da bu fonksiyonun true (doğru) döndürmesini sağlar return self:FindEnemy() -- Eğer düşman öldüyse (Alive() kullanmadan önce oyuncu olup olmadığını kontrol etmeliyiz) elseif ( self:GetEnemy():IsPlayer() and !self:GetEnemy():Alive() ) then return self:FindEnemy() -- Arama bir şey bulamazsa false (yanlış) döndür end -- Düşman ne çok uzakta ne de ölü, bu yüzden true (doğru) döndürebiliriz return true else -- Düşman geçerli değil, o yüzden yeni bir tane arayalım return self:FindEnemy() end end ---------------------------------------------------- -- ENT:FindEnemy() -- Bir tane bulursak true (doğru) döndürür ve düşmanımızı ayarlar ---------------------------------------------------- function ENT:FindEnemy() -- Etrafımızdaki varlıkları ara -- Bu, istediğiniz şekilde yapılabilir, örn. görme yetisini taklit etmek için ents.FindInCone() local _ents = ents.FindInSphere( self:GetPos(), self.SearchRadius ) -- Burada, yukarıdaki aramanın bulduğu her varlık arasında döngüye girip istediğimiz olup olmadığına bakıyoruz for k,v in ipairs( _ents ) do if ( v:IsPlayer() ) then -- Bir tane bulduk, o yüzden onu düşmanımız olarak ayarlayalım ve true (doğru) döndürelim self:SetEnemy(v) return true end end -- Hiçbir şey bulamadık, bu yüzden düşmanımızı nil (hiçbir şey) olarak ayarlayacağız ve false (yanlış) döndüreceğiz self:SetEnemy(nil) return false end

Coroutine'ler

Artık bot düşmanları bulabildiğine göre, sadece bir düşmana sahip olmaktan başka bir şey yapmasını sağlamalıyız. Bu sonraki bölüm, yapay zekamızın çoğunun kurulacağı yerdir. Fonksiyon bir coroutine'dir, yani coroutine.wait( zaman ) kullanarak belirli bir süre için duraklatabileceğiniz devasa bir döngüsel kod bölümüdür. Coroutine'ler, işleri zamanlanmış bir sırayla yapmanıza olanak tanır ve botun oyuncuya dönmesini veya bir animasyon oynatmasını sağlamak için fonksiyonu duraklatmanıza izin verir. Ve hepsi bir while true döngüsü içinde olduğu için, bot var olduğu sürece çalışacaktır. Yani yapay zekanız yapabileceği her şeyi çalıştırmayı bitirdikten sonra geri dönüp tekrar yapacaktır. İşte çok basit bir bot örneği.

function ENT:RunBehaviour() while ( true ) do -- İşte döngü, sonsuza kadar çalışacak self:StartActivity( ACT_WALK ) -- Yürüme animasyonu self.loco:SetDesiredSpeed( 200 ) -- Yürüme hızı self:MoveToPos( self:GetPos() + Vector( math.Rand( -1, 1 ), math.Rand( -1, 1 ), 0 ) * 400 ) -- Yaklaşık 400 birim içindeki rastgele bir yere yürü (yielding) self:StartActivity( ACT_IDLE ) -- Boşta durma animasyonu coroutine.wait(2) -- 2 saniye bekle coroutine.yield() -- Fonksiyon burada bitti, ancak döngünün başına geri dönecek ve botun başka bir yere yürümesini sağlayacak end end

Oldukça basit, değil mi? Rastgele bir noktaya yürü ve başka bir rastgele noktaya yürümeden önce 2 saniye boşta bekle. Bu ne kadar hoş olsa da istediğimiz şey bu değil, sırada çok daha iyi bir yapay zeka var.

Botumuzun "beyni"

Bu kod bazılarına ne kadar korkutucu görünse de, aslında oldukça basittir:

  • Bir düşmanımız olup olmadığını kontrol et, yoksa yukarıdaki HaveEnemy() fonksiyonunu kullanarak bir tane arayacak.
  • Eğer bir düşman varsa, bazı animasyonları oynat ve oyuncuya doğru koş.
  • Hiç düşman yoksa, rastgele bir noktaya yürü.
  • 2 saniye boşta bekle. O kadar da kötü değil, değil mi? Koda bir göz atın, her şeyin ne işe yaradığını bilmeniz için yorumlarla doldurdum.
---------------------------------------------------- -- ENT:RunBehaviour() -- Yapay zekamızın ana kısmı burasıdır ---------------------------------------------------- function ENT:RunBehaviour() -- Bu fonksiyon, varlık ilk spawn olduğunda çağrılır, NPC var olduğu sürece çalışacak devasa bir döngü görevi görür while ( true ) do -- Yukarıda bahsedilen fonksiyonları kullanarak bir düşmanımız olup olmadığını/bulup bulamayacağımızı görelim if ( self:HaveEnemy() ) then -- Artık bir düşmanımız olduğuna göre, bu bloktaki kod çalışacak self.loco:FaceTowards(self:GetEnemy():GetPos()) -- Düşmanımıza dön self:PlaySequenceAndWait( "plant" ) -- Bir düşman bulduğumuzu göstermek için bir poz verelim self:PlaySequenceAndWait( "hunter_angry" )-- Düşmana kızgın olduğumuzu göstermek için bir animasyon oynat self:PlaySequenceAndWait( "unplant" ) -- Pozdan çık self:StartActivity( ACT_RUN ) -- Animasyonu ayarla self.loco:SetDesiredSpeed( 450 ) -- Hareket edeceğimiz hızı ayarla. Endişelenmeyin, animasyon eşleşmek için hızlanacak/yavaşlayacaktır self.loco:SetAcceleration(900) -- Düşmana hızla koşacağız, bu yüzden gerçekten hızlı bir şekilde hızlanmak istiyoruz self:ChaseEnemy( ) -- Yakında incelenecek olan MoveToPos gibi yeni bir fonksiyon. self.loco:SetAcceleration(400) -- Düşmanı kovalamayı bitirdiğimiz için bunu varsayılan değerine geri ayarla self:PlaySequenceAndWait( "charge_miss_slide" ) -- Hareketi durdurduğumuzda havalı bir animasyon oynatalım self:StartActivity( ACT_IDLE ) --İşimiz bitti, o yüzden boşa dönelim -- Şimdi yukarıdaki fonksiyon yapması gerekeni bitirdiğinde, kod başlangıca geri dönecektir -- if ifadesinden sonra bir şeyler koymazsanız. O zaman döngüden önce o çalıştırılır else -- Bir düşman bulamadığımız için, gezinelim -- Garry'nin test botunda kullanılan kodun aynısı self:StartActivity( ACT_WALK ) -- Yürüme animasyonu self.loco:SetDesiredSpeed( 200 ) -- Yürüme hızı self:MoveToPos( self:GetPos() + Vector( math.Rand( -1, 1 ), math.Rand( -1, 1 ), 0 ) * 400 ) -- Yaklaşık 400 birim içindeki rastgele bir yere yürü (yielding) self:StartActivity( ACT_IDLE ) end -- Kodun bu noktasında bot, oyuncuyu kovalamayı bırakmış veya rastgele bir noktaya yürümeyi bitirmiştir -- Bu sonraki fonksiyonu kullanarak, devam edip tekrarlamadan önce 2 saniye bekleyeceğiz coroutine.wait(2) end end

ChaseEnemy() fonksiyonu nedir?

Kodu inceledikten sonra muhtemelen wiki'de hiçbir yerde olmayan ve bu eğitimde henüz yayınlanmamış bir fonksiyon fark ettiniz. ChaseEnemy(), şimdi inceleyeceğimiz şeydir; bazı kullanışlı değişiklikler dışında Garry'nin Nextbot tabanında yaptığı MoveToPos fonksiyonuyla hemen hemen aynıdır:

  • Doğrudan belirlenen düşmana giden bir takip yolu oluşturur.
  • Düşman ve bot hareket ettikçe yolu güncellemeye devam eder.
  • HaveEnemy() fonksiyonunu kullanarak, artık bir düşmanımız yoksa onu kovalamayı bırakır. İşte böyle görünüyor:
---------------------------------------------------- -- ENT:ChaseEnemy() -- Garry'nin MoveToPos fonksiyonuna benzer şekilde çalışır -- ancak sürekli olarak -- artık bir düşman kalmayana kadar -- düşmanın pozisyonunu takip eder. ---------------------------------------------------- function ENT:ChaseEnemy( options ) local options = options or {} local path = Path( "Follow" ) path:SetMinLookAheadDistance( options.lookahead or 300 ) path:SetGoalTolerance( options.tolerance or 20 ) path:Compute( self, self:GetEnemy():GetPos() ) -- Düşmanın konumuna doğru yolu hesapla if ( !path:IsValid() ) then return "failed" end while ( path:IsValid() and self:HaveEnemy() ) do if ( path:GetAge() > 0.1 ) then -- Oyuncuyu takip ettiğimiz için yolu sürekli olarak yeniden oluşturmalıyız path:Compute(self, self:GetEnemy():GetPos())-- Düşmanın konumuna doğru yolu yeniden hesapla end path:Update( self ) -- Bu fonksiyon botu yol boyunca hareket ettirir if ( options.draw ) then path:Draw() end -- Eğer takılırsak, HandleStuck fonksiyonunu çağır ve vazgeç if ( self.loco:IsStuck() ) then self:HandleStuck() return "stuck" end coroutine.yield() end return "ok" end

Garry'nin fonksiyonuna ve ardından buna bir göz atın ve nelerin daha ayrıntılı olarak değiştirildiğini görün.

Neredeyse bitti

Artık bot tamamlandığına göre, onu NPC spawn sekmesine eklemek isteyeceksiniz. Oldukça kolay, ama yine de bu eğitim için kullanılan kod burada:

list.Set( "NPC", "simple_nextbot", { Name = "Simple bot", Class = "simple_nextbot", Category = "Nextbot" })

Bunu kullandığınızda, her iki "simple_nextbot" ifadesini de botun dosya adıyla değiştirin, geri kalanını kendiniz çözmeniz oldukça kolaydır.

Görevler

Artık haritada dolaşan temel bir botunuz var ve hepsi bu kadar. İşte işleri biraz renklendirmek için kendi başınıza deneyebileceğiniz bazı şeyler:

  • Sadece oyunculardan daha fazlasını ara
  • Etrafta dolaşırken sesler çal.
  • Sadece önündeki düşmanları ara, etrafındakileri değil.
  • Düşman pompalı tüfek tutuyorsa saklanmasını sağla.
  • Düşmana çok yaklaştığında kovalamayı bırak ve bir yakın dövüş saldırısı yap.

Kodun tamamı

AddCSLuaFile() ENT.Base = "base_nextbot" ENT.Spawnable = true function ENT:Initialize() self:SetModel( "models/player/gman_high.mdl" ) self.LoseTargetDist = 2000 -- Düşmanı kaybetmeden önce ne kadar uzakta olması gerektiği self.SearchRadius = 1000 -- Ne kadar uzağa kadar düşman aranacağı end ---------------------------------------------------- -- ENT:Get/SetEnemy() -- Düşmanımızı kayıtlı tutmak için kullanılan basit fonksiyonlar ---------------------------------------------------- function ENT:SetEnemy(ent) self.Enemy = ent end function ENT:GetEnemy() return self.Enemy end ---------------------------------------------------- -- ENT:HaveEnemy() -- Eğer bir düşmanımız varsa true (doğru) döndürür ---------------------------------------------------- function ENT:HaveEnemy() -- Mevcut düşmanımız geçerliyse if ( self:GetEnemy() and IsValid(self:GetEnemy()) ) then -- Düşman çok uzaktaysa if ( self:GetRangeTo(self:GetEnemy():GetPos()) > self.LoseTargetDist ) then -- Düşman kaybedildiyse, yeni bir tane aramak için FindEnemy() fonksiyonunu çağır -- FindEnemy(), bir düşman bulunursa true (doğru) döndürür, bu da bu fonksiyonun true (doğru) döndürmesini sağlar return self:FindEnemy() -- Eğer düşman öldüyse (Alive() kullanmadan önce oyuncu olup olmadığını kontrol etmeliyiz) elseif ( self:GetEnemy():IsPlayer() and !self:GetEnemy():Alive() ) then return self:FindEnemy() -- Arama bir şey bulamazsa false (yanlış) döndür end -- Düşman ne çok uzakta ne de ölü, bu yüzden true (doğru) döndürebiliriz return true else -- Düşman geçerli değil, o yüzden yeni bir tane arayalım return self:FindEnemy() end end ---------------------------------------------------- -- ENT:FindEnemy() -- Bir tane bulursak true (doğru) döndürür ve düşmanımızı ayarlar ---------------------------------------------------- function ENT:FindEnemy() -- Etrafımızdaki varlıkları ara -- Bu, istediğiniz şekilde yapılabilir, örn. görme yetisini taklit etmek için ents.FindInCone() local _ents = ents.FindInSphere( self:GetPos(), self.SearchRadius ) -- Burada, yukarıdaki aramanın bulduğu her varlık arasında döngüye girip istediğimiz olup olmadığına bakıyoruz for k,v in ipairs( _ents ) do if ( v:IsPlayer() ) then -- Bir tane bulduk, o yüzden onu düşmanımız olarak ayarlayalım ve true (doğru) döndürelim self:SetEnemy(v) return true end end -- Hiçbir şey bulamadık, bu yüzden düşmanımızı nil (hiçbir şey) olarak ayarlayacağız ve false (yanlış) döndüreceğiz self:SetEnemy(nil) return false end ---------------------------------------------------- -- ENT:RunBehaviour() -- Yapay zekamızın ana kısmı burasıdır ---------------------------------------------------- function ENT:RunBehaviour() -- Bu fonksiyon, varlık ilk spawn olduğunda çağrılır. NPC var olduğu sürece çalışacak devasa bir döngü görevi görür while ( true ) do -- Yukarıda bahsedilen fonksiyonları kullanarak bir düşmanımız olup olmadığını/bulup bulamayacağımızı görelim if ( self:HaveEnemy() ) then -- Artık bir düşmanımız olduğuna göre, bu bloktaki kod çalışacak self.loco:FaceTowards(self:GetEnemy():GetPos()) -- Düşmanımıza dön self:PlaySequenceAndWait( "plant" ) -- Bir düşman bulduğumuzu göstermek için bir poz verelim self:PlaySequenceAndWait( "hunter_angry" )-- Düşmana kızgın olduğumuzu göstermek için bir animasyon oynat self:PlaySequenceAndWait( "unplant" ) -- Pozdan çık self:StartActivity( ACT_RUN ) -- Animasyonu ayarla self.loco:SetDesiredSpeed( 450 ) -- Hareket edeceğimiz hızı ayarla. Endişelenmeyin, animasyon eşleşmek için hızlanacak/yavaşlayacaktır self.loco:SetAcceleration(900) -- Düşmana hızla koşacağız, bu yüzden gerçekten hızlı bir şekilde hızlanmak istiyoruz self:ChaseEnemy( ) -- MoveToPos gibi yeni bir fonksiyon. self.loco:SetAcceleration(400) -- Düşmanı kovalamayı bitirdiğimiz için bunu varsayılan değerine geri ayarla self:PlaySequenceAndWait( "charge_miss_slide" ) -- Hareketi durdurduğumuzda havalı bir animasyon oynatalım self:StartActivity( ACT_IDLE ) --İşimiz bitti, o yüzden boşa dönelim -- Şimdi yukarıdaki fonksiyon yapması gerekeni bitirdiğinde, kod başlangıca geri dönecektir -- if ifadesinden sonra bir şeyler koymazsanız. O zaman döngüden önce o çalıştırılır else -- Bir düşman bulamadığımız için, gezinelim -- Garry'nin test botunda kullanılan kodun aynısı self:StartActivity( ACT_WALK ) -- Yürüme animasyonu self.loco:SetDesiredSpeed( 200 ) -- Yürüme hızı self:MoveToPos( self:GetPos() + Vector( math.Rand( -1, 1 ), math.Rand( -1, 1 ), 0 ) * 400 ) -- Yaklaşık 400 birim içindeki rastgele bir yere yürü (yielding) self:StartActivity( ACT_IDLE ) end -- Kodun bu noktasında bot, oyuncuyu kovalamayı bırakmış veya rastgele bir noktaya yürümeyi bitirmiştir -- Bu sonraki fonksiyonu kullanarak, devam edip tekrarlamadan önce 2 saniye bekleyeceğiz coroutine.wait(2) end end ---------------------------------------------------- -- ENT:ChaseEnemy() -- Garry'nin MoveToPos fonksiyonuna benzer şekilde çalışır -- ancak artık bir tane kalmayana kadar sürekli olarak -- düşmanın pozisyonunu takip eder. ---------------------------------------------------- function ENT:ChaseEnemy( options ) local options = options or {} local path = Path( "Follow" ) path:SetMinLookAheadDistance( options.lookahead or 300 ) path:SetGoalTolerance( options.tolerance or 20 ) path:Compute( self, self:GetEnemy():GetPos() ) -- Düşmanın konumuna doğru yolu hesapla if ( !path:IsValid() ) then return "failed" end while ( path:IsValid() and self:HaveEnemy() ) do if ( path:GetAge() > 0.1 ) then -- Oyuncuyu takip ettiğimiz için yolu sürekli olarak yeniden oluşturmalıyız path:Compute(self, self:GetEnemy():GetPos())-- Düşmanın konumuna doğru yolu yeniden hesapla end path:Update( self ) -- Bu fonksiyon botu yol boyunca hareket ettirir if ( options.draw ) then path:Draw() end -- Eğer takılırsak, HandleStuck fonksiyonunu çağır ve vazgeç if ( self.loco:IsStuck() ) then self:HandleStuck() return "stuck" end coroutine.yield() end return "ok" end list.Set( "NPC", "simple_nextbot", { Name = "Simple bot", Class = "simple_nextbot", Category = "Nextbot" })

Kullanışlı Konsol Komutları

İşte NextBot'larınızı oluşturmanıza ve hatalarını ayıklamanıza yardımcı olabilecek bazı kullanışlı konsol komutları.

Komut Açıklama
nb_debug <değerler> Nextbot'ların çeşitli bölümlerinde hata ayıklamanızı sağlar. Değerler bir boşlukla birleştirilebilir.



Olası değerler şunlardır:

BEHAVIOR, LOOK_AT, PATH, ANIMATION, LOCOMOTION, VISION, HEARING, EVENTS, ERRORS

Diğer Örnekler

Size yol göstermesi için bazı ek örnekler.

raubana'dan kullanışlı NextBot örnekleri

Tüm sunucular · GmodTR Launcher · Rehber · GMod Wiki · GmodTR nedir? · Sunucu ekle · Discord