Belki prosedür düzeyinde üretime aşinasınızdır; Bu yazıda her şey prosedürel görev oluşturmayla ilgili. Rogue benzeri oyunlar için klasik makine öğrenimini ve yinelenen sinir ağlarını kullanarak görevler oluşturmanın büyük resmini inceleyeceğiz.
Herkese selam! Adım Lev Kobelev ve MY.GAMES'te Oyun Tasarımcısıyım. Bu makalede, prosedürel görev oluşturmaya nasıl ve neden karar verdiğimizi açıklarken klasik makine öğrenimi ve basit sinir ağlarını kullanma deneyimimi paylaşmak istiyorum. Ayrıca sürecin Zombie'de uygulanmasına da derinlemesine bakacağız. Durum.
Yasal Uyarı: Bu makale yalnızca bilgilendirme/eğlence amaçlıdır ve belirli bir çözümü kullanırken, belirli bir kaynağın kullanım koşullarını dikkatlice kontrol etmenizi ve hukuk personeline danışmanızı öneririz!
☝🏻 Öncelikle bazı terminoloji: “ arenalar ”, “ seviyeler ” ve “ konumlar ” bu bağlamda eş anlamlı olduğu gibi “ alan ”, “ bölge ” ve “ doğma alanı ” da eşanlamlıdır.
Şimdi “ misyon ”u tanımlayalım. Görev, düşmanların belirli kurallara göre bir yerde belirdiği, önceden belirlenmiş bir düzendir . Daha önce de belirtildiği gibi, Zombie State'te konumlar oluşturuldu, dolayısıyla "aşamalı" bir deneyim yaratmıyoruz. Yani düşmanları önceden belirlenmiş noktalara yerleştirmiyoruz, aslında öyle bir nokta yok. Bizim durumumuzda, oyuncunun veya belirli bir duvarın yakınında bir yerde bir düşman beliriyor. Ayrıca oyundaki tüm arenalar dikdörtgen şeklinde olduğundan herhangi bir görev, herhangi birinde oynanabilir.
“ Doğma ” terimini tanıtalım. Yumurtlama, önceden belirlenmiş parametrelere göre belirlenmiş bir bölgedeki noktalarda aynı türden birkaç düşmanın ortaya çıkmasıdır . Bir puan – bir düşman. Bir alanın içinde yeterli nokta yoksa özel kurallara göre genişletilir. Bölgenin yalnızca bir yumurtlama tetiklendiğinde belirlendiğini anlamak da önemlidir. Alan, doğma parametrelerine göre belirlenir ve aşağıda iki örneği ele alacağız: oyuncunun yakınında ve bir duvarın yakınında ortaya çıkma.
İlk doğuş türü oyuncunun yakınındadır . Oyuncunun yakınındaki görünüm, iki yarıçapla tanımlanan bir sektör aracılığıyla belirlenir: dış ve iç (R ve r), sektörün genişliği (β), oynatıcıya göre dönme açısı (α) ve düşmanın görünümünün istenen görünürlüğü (veya görünmezliği). Bir sektörün içinde düşmanlar için gerekli sayıda puan bulunur ve bunlar da buradan gelir!
İkinci tür yumurtlama ise duvarın yakınındadır . Bir seviye oluşturulduğunda, her iki taraf da bir etiketle (ana yön) işaretlenir. Çıkışın bulunduğu duvar daima kuzeydedir. Bir duvarın yakınındaki düşmanın görünümü etiket, ondan olan mesafe (o), uzunluk (a), bölgenin genişliği (b) ve düşmanın görünümünün istenen görünürlüğü (veya görünmezliği) ile belirtilir. Bir bölgenin merkezi, oyuncunun mevcut konumuna göre belirlenir.
Yumurtlamalar dalgalar halinde gelir. Dalga, ortaya çıkma şeklidir, yani aralarındaki gecikmedir; oyuncuları tüm düşmanlarla aynı anda alt etmek istemiyoruz. Dalgalar görevlerde birleştirilir ve belirli bir mantığa göre birbiri ardına fırlatılır. Örneğin ikinci bir dalga, birinciden 20 saniye sonra (ya da içindeki zombilerin %90'ından fazlası öldürülürse) başlatılabilir. Yani, görevin tamamı büyük bir kutu olarak düşünülebilir ve bu kutunun içinde orta büyüklükte kutular (dalgalar) vardır ve dalgaların içinde daha da küçük kutular (doğanlar) vardır.
Dolayısıyla, görevler üzerinde çalışmaya başlamadan önce bazı kuralları zaten tanımladık:
Bir noktada yaklaşık yüz görevimiz hazırdı ama bir süre sonra daha fazlasına ihtiyaç duymaya başladık. Diğer tasarımcılar ve ben başka yüz görev oluşturmak için çok fazla zaman ve çaba harcamak istemedik, bu yüzden görev oluşturmak için hızlı ve ucuz bir yöntem aramaya başladık.
Tüm jeneratörler belirli kurallara göre çalışıyor ve manuel olarak oluşturduğumuz görevlerimiz de belirli önerilere göre yapılıyordu. Böylece, görevler içindeki kalıplarla ilgili bir hipotez ortaya attık ve bu kalıplar, jeneratör için kurallar olarak hareket edecek.
✍🏻 Metinde bulacağınız bazı terimler:
Kümeleme, belirli bir koleksiyonun örtüşmeyen alt kümelere (kümelere) bölünmesi, böylece benzer nesnelerin aynı kümeye ait olması ve farklı kümelerdeki nesnelerin önemli ölçüde farklı olması görevidir.
Kategorik özellikler, sonlu bir kümeden değer alan ve sayısal bir temsili olmayan verilerdir. Örneğin, doğma duvarı etiketi: Kuzey, Güney vb.
Kategorik özelliklerin kodlanması, kategorik özelliklerin önceden belirlenmiş bazı kurallara göre sayısal bir gösterime dönüştürülmesine yönelik bir prosedürdür. Örneğin, Kuzey → 0, Güney → 1 vb.
Normalleştirme, aralıklardaki fark hakkındaki bilgileri kaybetmeden bunları ortak bir ölçeğe getirmek amacıyla sayısal özelliklerin ön işleme yöntemidir. Örneğin nesnelerin benzerliğini hesaplamak için kullanılabilirler. Daha önce de belirtildiği gibi nesne benzerliği kümeleme problemlerinde önemli bir rol oynamaktadır.
Tüm bu kalıpları manuel olarak aramak son derece zaman alıcı olacaktır, bu nedenle kümelemeyi kullanmaya karar verdik. Bu görevi iyi bir şekilde yerine getirdiği için makine öğreniminin kullanışlı olduğu yer burasıdır.
Kümeleme bazı N boyutlu uzaylarda çalışır ve ML özellikle sayılarla çalışır. Bu nedenle tüm ortaya çıkmalar vektör haline gelecektir:
Yani örneğin “kuzey duvarında 2 metre girintili, 10 genişliğinde ve 5 uzunluğunda bir alanda 10 zombi atıcısı yumurtla” olarak tanımlanan yumurtlama, [0,5, 0,25, 0,2] vektörü haline geldi. , 0,8, …, 0,5] (←bu sayılar soyuttur).
Ek olarak, belirli düşmanların soyut türlerle eşleştirilmesiyle düşman grubunun gücü azaltıldı. Yeni başlayanlar için bu tür bir haritalama, belirli bir kümeye yeni bir düşman atamayı kolaylaştırdı. Bu aynı zamanda optimum kalıp sayısını azaltmayı ve sonuç olarak üretim doğruluğunu artırmayı da mümkün kıldı; ancak bu konuya daha sonra değineceğiz.
Birçok kümeleme algoritması vardır: K-Means, DBSCAN, spektral, hiyerarşik vb. Hepsi farklı fikirlere dayanıyor ancak aynı hedefe sahipler: verilerdeki kümeleri bulmak. Aşağıda, seçilen algoritmaya bağlı olarak aynı veriler için kümeleri bulmanın farklı yollarını görüyorsunuz.
K-Means algoritması, ortaya çıkma durumunda en iyi performansı gösterdi.
Şimdi, bu algoritma hakkında hiçbir şey bilmeyenler için küçük bir açıklama yapalım (bu makale makine öğreniminin temelleriyle ilgili değil, oyun geliştirmeyle ilgili olduğundan katı bir matematiksel akıl yürütme olmayacaktır). K-Means, her özellikten atanmış kümenin ortalama değerine kadar olan uzaklıkların karelerinin toplamını en aza indirerek verileri yinelemeli olarak K kümeye böler. Ortalama, kare mesafelerin küme içi toplamı ile ifade edilir.
Bu yöntemle ilgili aşağıdakileri anlamak önemlidir:
İkinci noktaya biraz daha detaylı bakalım.
Dirsek yöntemi genellikle optimum küme sayısını seçmek için kullanılır. Fikir çok basit: Algoritmayı çalıştırıyoruz ve 1'den N'ye kadar olan tüm K'ları deneriz; burada N makul bir sayıdır. Bizim durumumuzda bu sayı 10'du; daha fazla küme bulmak imkansızdı. Şimdi her küme içindeki mesafelerin karelerinin toplamını bulalım (WSS veya SS olarak bilinen bir puan). Tüm bunları bir grafikte görüntüleyeceğiz ve sonrasında y eksenindeki değerin önemli ölçüde değişmesinin durduğu bir nokta seçeceğiz.
Örnek olarak iyi bilinen bir veri kümesini kullanacağız:
Dirseği göremiyorsanız Silhouette yöntemini kullanabilirsiniz ancak bu yazının kapsamı dışındadır.
Yukarıdaki ve aşağıdaki hesaplamaların tümü Python'da makine öğrenimi ve veri analizi için standart kütüphaneler kullanılarak yapıldı: pandas, numpy, seaborn ve sklearn. Makalenin asıl amacı teknik ayrıntılara girmek yerine yetenekleri göstermek olduğundan kodu paylaşmıyorum.
Optimum küme sayısı elde edildikten sonra her birinin ayrıntılı olarak incelenmesi gerekir. İçerisinde hangi Spawn’ların yer aldığını ve aldıkları değerleri görmemiz gerekiyor. Daha sonraki nesillerde kullanmak üzere her küme için kendi ayarlarımızı oluşturalım. Parametreler şunları içerir:
Sözlü olarak "basit düşmanların oyuncunun yakınında kısa bir mesafede ve büyük olasılıkla görünür noktalarda ortaya çıkması" olarak tanımlanabilecek küme ayarlarını ele alalım.
Küme 1 tablosu
Düşmanlar | Tip | R | R-delta | rotasyon | Genişlik | görünürlük |
---|---|---|---|---|---|---|
zombi_common_3_5=4, zombi_heavy=1 | oyuncu | 10-12 | 1-2 | 0-30 | 30-45 | Görünür=9, Görünmez=1 |
İşte iki yararlı püf noktası:
Bu, her küme için yapıldı ve sayıları 10'dan azdı, dolayısıyla uzun sürmedi.
Bu konuya sadece biraz değindik ama hala araştırılacak pek çok ilginç şey var. İşte referans olabilecek bazı makaleler; verilerle çalışma, kümeleme ve sonuçları analiz etme süreçlerinin iyi bir tanımını sağlarlar.
Ortaya çıkma modellerine ek olarak, bu parametreyi üretim sırasında kullanmak için, bir görev içindeki düşmanların toplam sağlığının, görevin beklenen tamamlanma süresine bağımlılığını incelemeye karar verdik.
Manuel görevler oluşturma sürecinde görev, bölüm için koordineli bir tempo oluşturmaktı; bir dizi görev: kısa, uzun, kısa, tekrar kısa vb. Oyuncunun beklenen DPS'sini ve süresini biliyorsanız, bir görevdeki düşmanların toplam sağlığını nasıl elde edebilirsiniz?
💡 Doğrusal regresyon, bir değişkenin diğerine veya birkaç başka değişkene bağımlılığını doğrusal bir bağımlılık fonksiyonuyla yeniden yapılandırmanın bir yöntemidir. Aşağıdaki örneklerde yalnızca bir değişkenden doğrusal regresyon ele alınacaktır: f(x) = wx + b.
Aşağıdaki terimleri tanıtalım:
Yani, HP = DPS * eylem süresi + boş zaman. Manuel bir bölüm oluştururken her görevin beklenen süresini kaydettik; şimdi harekete geçme zamanı bulmamız gerekiyor.
Beklenen görev süresini biliyorsanız, eylem süresini hesaplayabilir ve boş zaman elde etmek için bunu beklenen süreden çıkarabilirsiniz: serbest zaman = görev süresi - eylem süresi = görev süresi - HP * DPS. Bu sayı daha sonra görevdeki ortalama düşman sayısına bölünebilir ve düşman başına serbest zaman elde edersiniz. Bu nedenle geriye kalan tek şey, beklenen görev süresinden düşman başına serbest zamana kadar doğrusal bir regresyon oluşturmaktır.
Ek olarak, görev süresinden eylem süresinin payına ilişkin bir regresyon oluşturacağız.
Bir hesaplama örneğine bakalım ve bu regresyonların neden kullanıldığını görelim:
İşte bir soru: Neden düşmanın boş zamanını bilmemiz gerekiyor? Daha önce de belirtildiği gibi, yumurtlamalar zamana göre düzenlenir. Bu nedenle, i'inci ortaya çıkma süresi, (i-1)'inci ortaya çıkmanın eylem süresi ile içindeki boş zamanın toplamı olarak hesaplanabilir.
Ve burada başka bir soru ortaya çıkıyor: Eylem süresi ile boş zamanın payı neden sabit değil?
Oyunumuzda bir görevin zorluğu süresiyle ilgilidir. Yani kısa görevler daha kolay, uzun görevler ise daha zordur. Zorluk parametrelerinden biri düşman başına düşen serbest zamandır. Yukarıdaki grafikte birkaç düz çizgi vardır ve bunlar aynı eğim katsayısına (w) fakat farklı bir uzaklığa (b) sahiptir. Bu nedenle, zorluğu değiştirmek için dengeyi değiştirmek yeterlidir: b'yi artırmak oyunu kolaylaştırır, azaltmak oyunu zorlaştırır ve negatif sayılara izin verilir. Bu seçenekler, zorluğu bölümden bölüme değiştirmenize yardımcı olur.
Tüm tasarımcıların regresyon sorununu derinlemesine incelemeleri gerektiğine inanıyorum, çünkü bu genellikle diğer projelerin yapısökümüne yardımcı olur:
Böylece jeneratörün kurallarını bulmayı başardık ve artık üretim sürecine geçebiliriz.
Soyut düşünürseniz, herhangi bir görev, her sayının belirli bir ortaya çıkma kümesini yansıttığı bir sayı dizisi olarak temsil edilebilir. Örneğin görev: 1, 2, 1, 1, 2, 3, 3, 2, 1, 3. Bu, yeni görevler oluşturma görevinin yeni sayısal diziler oluşturmaya bağlı olduğu anlamına gelir. Oluşturma sonrasında, küme ayarlarına uygun olarak her sayıyı ayrı ayrı "genişletmeniz" yeterlidir.
Bir dizi oluşturmanın önemsiz bir yöntemini düşünürsek, belirli bir ortaya çıkmanın başka herhangi bir ortaya çıkmayı takip etmesinin istatistiksel olasılığını hesaplayabiliriz. Örneğin, aşağıdaki diyagramı elde ederiz:
Diyagramın üst kısmı, onun yol açtığı bir küme, bir tepe noktasıdır ve kenar ağırlığı, kümenin bir sonraki olma olasılığıdır.
Böyle bir grafiği inceleyerek bir dizi oluşturabiliriz. Ancak bu yaklaşımın bir takım dezavantajları vardır. Bunlar arasında, örneğin hafıza eksikliği (yalnızca mevcut durumu bilir) ve kendine dönüşme istatistiksel olasılığı yüksekse, bir durumda "sıkışıp kalma" şansı yer alır.
✍🏻 Bu grafiği bir süreç olarak düşünürsek basit bir Markov zinciri elde ederiz.
Temel yaklaşımın dezavantajlarına sahip olmadıkları için sinir ağlarına, yani tekrarlayan ağlara dönelim. Bu ağlar, doğal dil işleme görevlerindeki karakterler veya kelimeler gibi dizileri modellemede iyidir. Çok basit bir şekilde ifade etmek gerekirse ağ, öncekilere dayanarak dizinin bir sonraki öğesini tahmin edecek şekilde eğitilir.
Bu ağların nasıl çalıştığının açıklaması bu makalenin kapsamı dışındadır çünkü bu çok büyük bir konudur. Bunun yerine, eğitim için nelerin gerekli olduğuna bakalım:
N=2, L=3, C=5 ile basit bir örnek. 1, 2, 3, 4, 1 dizisini alalım ve onun içinde L+1 uzunluğunda alt dizileri arayalım: [1, 2, 3, 4], [2, 3, 4, 1]. Diziyi L karakterlerinden oluşan bir girişe ve bir yanıta (hedef) - (L+1)'inci karaktere* bölelim.* Örneğin, [1, 2, 3, 4] → [1, 2, 3] ve [ 4]. Cevapları tek sıcak vektörlere kodluyoruz, [4] → [0, 0, 0, 0, 1].
Daha sonra tensorflow veya pytorch kullanarak Python'da basit bir sinir ağının taslağını çizebilirsiniz. Aşağıdaki bağlantıları kullanarak bunun nasıl yapıldığını görebilirsiniz. Geriye kalan tek şey yukarıda açıklanan veriler üzerinde eğitim sürecini başlatmak, beklemek ve... ardından üretime geçebilirsiniz!
Makine öğrenimi modellerinin doğruluk gibi belirli ölçümleri vardır. Doğruluk, doğru verilen cevapların oranını gösterir. Ancak verilerde sınıf dengesizlikleri olabileceğinden dikkatli bir şekilde incelenmelidir. Eğer hiç yoksa (veya hemen hemen hiç yoksa), o zaman cevapları rastgele tahmin etmekten daha iyi tahmin ediyorsa, yani doğruluk > 1/C ise modelin iyi çalıştığını söyleyebiliriz; 1'e yakınsa harika çalışır.
Bizim durumumuzda model iyi bir doğruluk gösterdi. Bu sonuçların nedenlerinden biri, düşmanların türlerine ve dengelerine göre haritalanması sayesinde elde edilen küme sayısının az olmasıdır.
İlgilenenler için RNN hakkında daha fazla materyal:
Eğitilen model kolaylıkla
Modelle etkileşim kurmak için Unity'de oyun tasarımcılarının gerekli tüm görev parametrelerini ayarlayabileceği özel bir pencere oluşturulur:
Ayarları girdikten sonra geriye sadece bir butona basmak ve gerektiğinde düzenlenebilecek bir dosya almak kalıyor. Evet, görevleri oyun sırasında değil önceden oluşturmak istedim, böylece üzerlerinde değişiklik yapılabilir.
Üretim sürecine bakalım:
Yani bu, görevlerin oluşturulmasını birkaç kez hızlandırmamıza yardımcı olan iyi bir araçtır. Ek olarak, bazı tasarımcıların deyim yerindeyse “yazar tıkanıklığı” korkusunun üstesinden gelmelerine de yardımcı oldu, çünkü artık birkaç saniye içinde hazır bir çözüme sahip olabiliyorsunuz.
Yazıda görev oluşturma örneğini kullanarak klasik makine öğrenmesi yöntemlerinin ve sinir ağlarının oyun geliştirmede nasıl yardımcı olabileceğini göstermeye çalıştım. Bugünlerde üretken yapay zekaya doğru büyük bir eğilim var; ancak makine öğreniminin diğer dallarını da unutmayın çünkü onlar da pek çok şey yapabilir.
Bu makaleyi okumaya zaman ayırdığınız için teşekkür ederiz! Umarım hem oluşturulan konumlardaki görevlere yaklaşım hem de görevlerin oluşturulması hakkında fikir sahibi olursunuz. Yeni şeyler öğrenmekten korkmayın, kendinizi geliştirin ve güzel oyunlar yapın!
İllüstrasyonlar shabbyrtist'e ait