Godot Engine Eğitim Serisi - Bölüm 10: 3D Düşmanlar, Dinamik Spawn Sistemi, Zıplama, Ezme ve Çarpışma Maskeleri
Godot 3D'de düşman spawn sistemi, zıplama, ezme ve çarpışma maskeleri: Path3D, Dot Product ve fizik katmanları. Türkçe oyun geliştirme.
Oyuncu karakterimizi başarıyla 3D dünyaya entegre ettik. Şimdi oyuncunun kaçması ve üstüne atlayarak ezmesi gereken düşmanları (mob) oluşturma zamanı! Bu kapsamlı rehberde, önce 3D düşman sahnemizi tasarlayacak, ardından bu düşmanları oyun alanının etrafından rastgele konumlarda üretecek (spawn edecek) dinamik bir sistem kuracağız.
3D Düşman (Mob) Sahnesini İnşa Etmek
Düşmanlar için oyuncu sahnesine oldukça benzer bir yapı kuracağız. Yeni bir sahne oluşturun ve kök node olarak CharacterBody3D ekleyip adını Mob yapın.
- Tıpkı oyuncuda yaptığımız gibi, modeli kod ile kolayca döndürebilmek için
Mobnode’una bir Node3D çocuğu ekleyin ve adınıPivotyapın. - Dosya sisteminizdeki (
art/klasörü)mob.glbdosyasınıPivotnode’unun üzerine sürükleyerek 3D modeli sahnenize ekleyin. Oluşan node’un adınıCharacterolarak değiştirebilirsiniz.
mob.glb dosyasını Pivot üzerine sürükleyerek 3D modeli ekliyoruz
Mob > Pivot > Character yapısı
- Düşmanın fiziksel bir hacme sahip olması için
Mobnode’una bir CollisionShape3D ekleyin. - Inspector (Denetçi) panelinden şekil (Shape) olarak BoxShape3D (Kutu) atayın ve kutuyu 3D modele uyacak şekilde turuncu noktalarından sürükleyerek boyutlandırın.
CollisionShape3D için BoxShape3D seçiyoruz
Çarpışma kutusu modeli hafifçe sarıyor — tabana değiyor, yanlarda biraz daha dar
💡 İpucu: Çarpışma kutusunu modelden çok az daha dar yapmanız oyuncuya haksızlık yapılmasını önler. Eğer kutu modelden büyük olursa, oyuncu düşmana görsel olarak değmeden de hasar almış gibi hissedebilir.
Bellek Yönetimi: Ekrandan Çıkanları Silmek
Düşmanları sürekli olarak sahneye süreceğimiz için, ekranın dışına çıkan düşmanları silmezsek oyunumuzun belleği kısa sürede dolar ve oyun yavaşlar.
Bunu önlemek için Mob node’una bir VisibleOnScreenNotifier3D çocuğu ekleyin. Ekranda beliren pembe kutuyu, tüm 3D modeli kapsayacak şekilde büyütün.
VisibleOnScreenNotifier3D eklendi — pembe bir kutu belirir
Pembe kutu mob modelini tam olarak kaplıyor —
Düşman Yapay Zekasını Kodlamak
Mob node’una sağ tıklayarak yeni bir script ekleyin. Düşmanlarımızın mantığı basit olacak: Belirli bir konumda doğacaklar, oyuncuya doğru yönelecekler ve rastgele bir hızla düz bir çizgide ilerleyecekler.
Aşağıdaki kodu scriptinize yapıştırın:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extends CharacterBody3D
@export var min_speed = 10
@export var max_speed = 18
func _physics_process(_delta):
# Düşmanı her karede hareket ettiriyoruz
move_and_slide()
# Bu fonksiyon, düşman sahnede oluşturulduğunda Main sahnesi tarafından çağrılacak
func initialize(start_position, player_position):
# Düşmanı başlangıç noktasına yerleştir ve oyuncuya bakmasını sağla
look_at_from_position(start_position, player_position, Vector3.UP)
# Doğrudan oyuncuya gitmemesi için ±45 derecelik rastgele bir açı sapması ekle
rotate_y(randf_range(-PI / 4, PI / 4))
# Rastgele bir hız belirle ve vektörü düşmanın baktığı yöne çevir
var random_speed = randi_range(min_speed, max_speed)
velocity = Vector3.FORWARD * random_speed
velocity = velocity.rotated(Vector3.UP, rotation.y)
Kodun Satır Satır Açıklaması:
extends CharacterBody3D: Bu kodun 3D dünyadaki bir düşman gövdesini (Mob) kontrol edeceğini gösterir.@export var min_speed = 10&@export var max_speed = 18: Düşmanların hangi hız aralığında ekrana geleceğini Inspector (Denetçi) panelinden kolayca değiştirebilmek için@exportile tanımladığımız minimum ve maksimum sürat değişkenleridir.func _physics_process(_delta):: Motorun sürekli çalışan 3D fizik döngüsü. İçindeki_deltaparametresinin başına alt çizgi koyduk çünkü döngü içinde o geçici süreyi (delta’yı) bilerek kullanmıyoruz ve Godot’nun bize “Bu değişkeni hiç kullanmadın” diye uyarı (warning) vermesini engelliyoruz.move_and_slide(): Düşmanın, önceden belirlenmiş hızını (velocity) uygulayarak ileri doğru hareket etmesini sağlar.func initialize(start_position, player_position):: Bu fonksiyon motor tarafından otomatik çalışmaz. Özel olarak bizim yazdığımız, düşman ekranda doğduğunda (spawn) ona “Nereden çıkacaksın ve oyuncu şu an nerede?” (start & player_position) bilgisini verdiğimiz başlatıcı/kurucu fonksiyondur.look_at_from_position(...): Üç parametre alır. Düşmanın konumunu önce ekrandaki rastgele çıkış noktasına (start_position) koy, sonra yüzünü doğrudan oyuncunun o anki koordinatlarına (player_position) çevir.Vector3.UPise “Karakterin kafası yukarı tarafa gelsin, yan veya ters dönmesin” demek için eklenen referans bir diklik vektörüdür.rotate_y(randf_range(-PI / 4, PI / 4)): Yüzümüzü tamamen oyuncuya dönmüştük ama böyle yaparsak tüm düşmanlar tren gibi aynı düz çizgide gelir ve kolay lokma olurlar. Y açısı (sağa-sola dönme) ekseninde-45ile+45derece arasında (PI/4radyan) rastgele bir sapma ekliyoruz ki biraz çapraza doğru gitsinler.var random_speed = randi_range(min_speed, max_speed): En başta belirlediğimiz 10 ile 18 arasında rastgele tam bir sayı (örneğin 14) seç.velocity = Vector3.FORWARD * random_speed: 3D’de ileri yönü temsil edenFORWARDvektörünü (Z ekseninde -1 gidiş), seçtiğimiz bu rastgele hız ile çarp. Yani “Düz ileri gitme gücü” hesapla.velocity = velocity.rotated(Vector3.UP, rotation.y): Saf “düz ileri” gitme gücünü, yukarıda rastgele hesapladığımız kendi kafa açısına (rotation.y) çevir ve nihai hareket hızını (velocity) netleştir. Artık_physics_processiçindekimove_and_slidemotoru bu hızı kullanarak düşmanı her saniye yürütecek.
Bu kodda neler yaptık?
look_at_from_position()metoduyla düşmanı doğduğu noktadan doğrudan oyuncuya çevirdik.- Ancak her düşmanın ip gibi oyuncuyu takip etmemesi için
rotate_yile rotasyonuna rastgele bir sapma (-45 ile +45 derece arası) ekledik. velocity.rotated()fonksiyonuyla hız vektörünü düşmanın yönüne uyarladık.
Son olarak bellek yönetimi için VisibleOnScreenNotifier3D node’unuzu seçin, sağdaki Signals sekmesinden screen_exited sinyalini Mob scriptinize bağlayın. Oluşan fonksiyona queue_free() yazarak düşmanın ekrandan çıktığında silinmesini sağlayın. Sahnenizi mob.tscn olarak kaydetmeyi unutmayın.
3D Dinamik Spawn (Oluşturucu) Sistemi
Düşman sahnesi hazır olduğuna göre, onları rastgele konumlarda oyun alanına getirecek sistemi Main (Ana) sahnemizde kurmalıyız.
Mainsahnenizi açın.- Düşmanların ekran sınırlarının hemen dışından gelmesi için bir rota çizeceğiz.
Mainnode’una bir Path3D çocuğu ekleyin ve adınıSpawnPathyapın. SpawnPath‘in altına da bir PathFollow3D çocuğu ekleyip adınıSpawnLocationyapın.
SpawnPath ve SpawnLocation — spawn mekanizması için hazır
⚠️ Uyarı: 3D uzayda yol çizerken nereye tıkladığınızı tam göremeyebilirsiniz. Kameranızın görüş açısının dört köşesine geçici olarak silindir (CylinderMesh) nesneleri koyarak kendinize referans noktaları oluşturabilir ve Add Point aracıyla bu silindirlerin etrafından saat yönünde bir dörtgen çizebilirsiniz. Yolunuzu tamamladıktan sonra Close Curve (Yolu Kapat) butonuna tıklamayı unutmayın.
Dört silindir kamera görünümünün dört köşesinde
Spawn yolu kamera görünümünü çerçeveleyen bir dörtgen
Timer (Zamanlayıcı) Eklemek
- Düşmanların belirli aralıklarla gelmesi için
Mainnode’una bir Timer ekleyin ve adınıMobTimeryapın. - Inspector panelinden Wait Time (Bekleme Süresi) değerini
0.5yapın. - Autostart (Otomatik Başlat) özelliğini açık konuma getirin.
Wait Time: 0.5, Autostart: On —
Oluşturucu (Spawner) Kodunu Yazmak
Şimdi Main node’unuza bir script ekleyin. Scriptinizin en üstüne şu değişkeni tanımlayın:
1
2
3
extends Node
@export var mob_scene: PackedScene
Kodun Satır Satır Açıklaması:
extends Node: Oyunun yöneticisi olanMainadlı kök node’a bağlandığımızı gösterir.@export var mob_scene: PackedScene: Tıpkı 2D oyunumuzda olduğu gibi, düşman iskeletini (mob.tscn) dışarıdan sürükleyip bırakabilmemiz için açtığımız özel@exportkutusudur. “PackedScene” olması gerektiğini belirterek, içine yanlışlıkla görsel veya ses atılmasını engelleriz.
Bu kod sayesinde Inspector panelinde Mob Scene adında bir alan açılacaktır. Dosyalarınızdan mob.tscn sahnesini sürükleyip bu alana bırakın.
Son adım olarak, MobTimer node’unun timeout sinyalini Main scriptinize bağlayın ve fonksiyonun içini aşağıdaki gibi doldurun:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func _on_mob_timer_timeout():
# Düşman şablonundan yeni bir kopya oluştur
var mob = mob_scene.instantiate()
# PathFollow3D'yi çizdiğimiz yol üzerinde rastgele bir noktaya taşı (0.0 ile 1.0 arası)
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
mob_spawn_location.progress_ratio = randf()
# Oyuncunun anlık konumunu al
var player_position = $Player.position
# Düşmanın kendi scriptindeki initialize fonksiyonunu çağırarak konumunu ve rotasını ayarla
mob.initialize(mob_spawn_location.position, player_position)
# Düşmanı sahneye ekle
add_child(mob)
Kodun Satır Satır Açıklaması:
func _on_mob_timer_timeout():: Eklediğimiz Timer (Zamanlayıcı) düğümünün her süresi dolduğunda (örneğin yarım saniyede bir) otomatik tetiklediği sinyal fonksiyonudur. Düşman ekleme fabrikamız çalışır.var mob = mob_scene.instantiate(): Editöre sürükleyip verdiğimiz düşman (.tscn) kalıbını al, ondan bir tane daha yepyeni kopyala/yarat (.instantiate()) ve adınamobde.var mob_spawn_location = ...: Yukarıda ekran çevresine manuel olarak çizdiğimiz yolu ve üzerindeki kayan noktayı (PathFollow3D) ismine göre bul.`mob_spawn_location.progress_ratio = randf(): O kayan noktayı yolun tamamı olan 0.0 (Başlangıç) ile 1.0 (Son) arasında tamamen rastgele bir yere (randf()) ışınla.var player_position = $Player.position: Oyuncu karakterin sahnedeki tam o saniyedeki 3D koordinatlarını (X, Y, Z konumunu) kopyalayıp çantaya koy.mob.initialize(...): Yarattığımız yeni düşmanın içine gizli kodlara ulaşıyoruz. Yukarıda açıkladığımız özel başlatıcı fonksiyonuna sırasıyla;1. Doğman gereken yer burası (sınırda rastgele seçilen konum)ve2. Oyuncu şu an burada, oraya dön!parametrelerini yolluyoruz. Düşman bu verileri alıp kendi sağa/sola sapmasını ve hızını ayarlıyor.add_child(mob): Nihayet tüm işlemleri bitmiş, fırından yeni çıkmış düşmanı ana sahneye (Main) gerçek bir çocuk olarak ekle (“Motor, ışık, kayıt!”). Artık ekranda koşmaya başlayacaktır.
Düşmanlarımızı 3D dünyamıza başarıyla dahil ettik ve spawn sistemini kurduk. Ancak oyunun temel eğlencesi henüz eksik: Karakterimiz zıplayamıyor, düşmanları ezemiyor ve onlara çarptığında ölmüyor.
Şimdi Godot’nun fizik katmanlarını (Layers ve Masks) yapılandıracak, ardından matematiksel vektör işlemleriyle (Dot Product) zıplama ve ezme mekaniklerini kodlayacağız.
Fizik Katmanları (Layers) ve Maskeleri (Masks)
Godot’daki fizik gövdeleri iki tamamlayıcı özelliğe sahiptir: Layer (Katman) ve Mask (Maske).
- Layer: Nesnenin fiziksel olarak hangi katmanda bulunduğunu tanımlar.
- Mask: Nesnenin hangi katmanları “dinleyeceğini”, yani hangileriyle çarpışma algılayacağını belirler.
İki nesnenin etkileşebilmesi için en az birinin, diğerinin katmanına karşılık gelen bir maskesi olması zorunludur. Bu sistemi düzenli tutmak için katmanlarımıza isim verelim:
- Project > Project Settings menüsünü açın.
- Sol menüden Layer Names > 3D Physics bölümüne gidin.
- İlk üç katmanı sırasıyla
player,enemiesveworldolarak adlandırın.
3D Physics katman isimlendirme ekranı
Katman Atamalarını Yapmak
Şimdi sahnelerimizdeki node’ların katmanlarını bu yeni isimlendirmelere göre ayarlayalım:
- Ground (Zemin):
main.tscniçindeki Ground node’unu seçin. Layer olarak yalnızca 3’ü (world) işaretleyin ve Mask ayarlarının tamamını kapatın (çünkü zemin hiçbir şeyi dinlemez, sadece üstüne basılır).
Ground için Layer = world, Mask = boş
- Player:
player.tscniçindeki Player node’unu seçin. Layer olarak 1 (player), Mask olarak iseenemiesveworldkatmanlarını işaretleyin.
Player: Layer = player, Mask = enemies + world
- Mob:
mob.tscniçindeki Mob node’unu seçin. Layer olarakenemiesseçin, Mask ayarlarını ise tamamen kapatın ki mob’lar birbirlerine veya zemine sürtünmesin.
Mob: Layer = enemies, Mask = boş
Zıplama Mekaniği
Karakterin zıplaması için player.gd script dosyamızı açalım. Öncelikle değişkenlerinizin arasına zıplama gücünü belirten şu değişkeni ekleyin:
1
@export var jump_impulse = 20
Kodun Satır Satır Açıklaması:
@export: Bu değişkenin arayüzden (Inspector) değiştirilebilir olmasını sağlar.jump_impulse = 20: Karakterin zıplama gücü (ivmesi). Zıpla tuşuna basıldığında karakteri ne kadar yukarı atacağımızı temsil eder. Y ekseninde 20 birimlik bir zıplama kuvveti oluştururuz.
Ardından, _physics_process() fonksiyonunun içinde, move_and_slide() çağrısından hemen önce şu kodu ekleyin:
1
2
if is_on_floor() and Input.is_action_just_pressed("jump"):
target_velocity.y = jump_impulse
Kodun Satır Satır Açıklaması:
if is_on_floor() and Input.is_action_just_pressed("jump"):: Burada iki şartımız var. Birincisiis_on_floor()yani karakterin ayağı tamamen yere basıyor mu? Havada değilse. İkincisiand(ve) diyoruz, oyunu oynayan kişi “jump” (örneğin Boşluk) tuşunajust_pressed(tam o karede henüz basılmış mı) kontrolü.is_action_pressedile arasındaki fark,just_pressed‘in tuşa basılı tutsanız bile sadece ilk basıldığında bir kere çalışmasıdır.target_velocity.y = jump_impulse: Yukarıdaki iki şart da doğruysa (yere basıyorsa VE o an zıplamaya bastıysa) hedef hızımızın Y eksenine (yukarı yöne) az önce belirlediğimiz20değerini aktarıyoruz. Karakter füze gibi yukarı fırlar. Fizik motoru zaten sonraki karelerde yerçekimi uygulayarak bu 20 değerini yavaş yavaş sıfıra, sonra eksiye indirip onu yere indirecektir.
💡 Bilgilendirme: 3D dünyada Y ekseni yukarıya doğru pozitiftir.
is_on_floor()metodu sayesinde oyuncunun havada art arda zıplamasını (double jump) engellemiş oluyoruz.
Düşmanları Ezme (Squash) Mekaniği
Oyuncunun düşmanı ezdiğini algılaması için öncelikle düşmanları bir grup altında toplamalıyız. mob.tscn sahnesini açın, kök Mob node’unu seçip sağdaki Groups sekmesinden mob adında yeni bir grup oluşturarak node’u bu gruba dâhil edin.
“mob” grubu başarıyla oluşturuldu Şimdi player.gd scriptinize geri dönün ve düşman ezildiğinde karakterin hafifçe tekrar zıplamasını sağlayacak değişkeni ekleyin:
1
@export var bounce_impulse = 16
Kodun Satır Satır Açıklaması:
@export var bounce_impulse = 16: Düşmanı ezdiğimizde (kafasına bastığımızda) oyuncunun zafer zıplaması yaparak havaya hafiften tekrar sıçraması için16gücünde bir “sekme ivmesi” tanımlarız.
_physics_process() fonksiyonu içinde, zıplama kodunuzun hemen sonrasına şu döngüyü ekleyin:
1
2
3
4
5
6
7
8
9
10
11
12
for index in range(get_slide_collision_count()):
var collision = get_slide_collision(index)
if collision.get_collider() == null:
continue
if collision.get_collider().is_in_group("mob"):
var mob = collision.get_collider()
if Vector3.UP.dot(collision.get_normal()) > 0.1:
mob.squash()
target_velocity.y = bounce_impulse
break
Kodun Satır Satır Açıklaması:
for index in range(get_slide_collision_count()):: Karakter hermove_and_slide()yaptığında bir yerlere (zemine, duvara, düşmana) sürtebilir.get_slide_collision_count()o sürtünmelerin sayısını verir. Biz de birfordöngüsü kurup “1’den çarpışma sayısına kadar tüm temasları tek tek dön” diyoruz. (Örneğin 2 düşmana ve 1 duvara aynı anda değiyorsa, döngü 3 kez çalışır).var collision = get_slide_collision(index): Döngünün o anki temas bilgisini (örneğin ilk temas) alırcollisionadlı bir kutuya kaydederiz. İçinde “Neye çarptık? Neresinden çarptık? Hangi açıyla çarptık?” gibi bilgiler var.if collision.get_collider() == null: continue: Çok nadir durumlarda (örneğin çarptığımız şey o an silindiyse) çarptığımız nesnenull(boş/hiçlik) olabilir. Eğer öğle bir durum varsa hiç hata verme,continuediyerek bu teması atla ve döngüdeki sıradaki teması incele diyoruz.if collision.get_collider().is_in_group("mob"):: Çarptığımız bu nesnenin.is_in_group("mob")yani yapıldığında “mob” kalabalıklar/düşmanlar grubunda olup olmadığını sorarız.var mob = collision.get_collider(): Zemin değil bir düşman olduğunu anladığımıza göre, bu çarptığımız nesneyi geçici olarakmobisminde bir kutuya atarız ki üzerinde rahatça işlem yapabilelim.if Vector3.UP.dot(collision.get_normal()) > 0.1:: Çok kritik bir satır. Bize yandan mı çarptı, üstüne mi bastık ayrımını yapar.Vector3.UPdümdüz yukarı bakan bir oktur.collision.get_normal()ise düşman yüzeyinin “Bana nereden değdin?” okudur. Kafasına bastıysanız bu oklar aynı yöne bakar ve matematiksel Dot Product işlemi 1’e çok yakın (0.1’den büyük) bir değer döndürür. Yanlardan çarptıysanız oklar dik açıyla kesişeceği için değer 0.1’den küçük olur, yani bu eğer bloğuna girmez.mob.squash(): Eğer cidden kafasına bastıysak, az önce kutuya kaydettiğimiz düşmana kendi sahip olduğu “ezil (.squash())” komutunu yolluyoruz.target_velocity.y = bounce_impulse: Düşmanı ezdik, oyuncunun ödülü isebounce_impulse(sekme ivmesi olan 16) gücünde bir trambolin etkisi yaratılarak havaya zıplatılması.break: İşlemi başarıyla hallettiğimiz için zıpladık, bir karede iki kez üst üste ölümcül aynı işlemi yapmamak içinbreakile döngüyü tamamen sonlandırır ve oradan çıkarız.
Bu kodda neler yaptık?
get_slide_collision_count()veget_slide_collision()fonksiyonları ile karakterin o karedeki tüm çarpışmalarını tek tek inceliyoruz.- Çarptığımız nesne
mobgrubundaysa bir sonraki aşamaya geçiyoruz. - Dot Product (Nokta Çarpımı):
Vector3.UP.dot(collision.get_normal()) > 0.1matematiği, iki vektör arasındaki açıyı ölçer. Çarpışma yüzeyinin normali yukarıya bakıyorsa (yani düşmanın tepesine basıyorsak) bu değer 0.1’den büyük çıkar ve ezme işlemini doğrularız.
Mob Scriptini Güncellemek
Player, mob.squash() fonksiyonunu çağırıyor ancak bu fonksiyon henüz düşman scriptinde yok. mob.gd dosyasını açın, en üste kendi sinyalinizi tanımlayın:
1
signal squashed
Kodun Satır Satır Açıklaması:
signal squashed: Düşman dosyasında tanımlanan yeni, kendimize ait bir sinyal. “Ezildim/Yok edildim” anlamına gelir. Oyuncudanmob.squash()emri gelince bu sinyali ateşleyeceğiz. (Örneğin skor sistemi “bir düşman ezildim sinyali yaydıysa bana 10 puan ver” demek için bunu bekliyor olacak).
Ve scriptin en altına şu fonksiyonu ekleyin:
1
2
3
func squash():
squashed.emit()
queue_free()
Kodun Satır Satır Açıklaması:
func squash():: Oyuncunun tepeden bastığında bulup tetikleyeceği fonksiyon budur. Düşman ezildiğinde ne olacağını yazarız.squashed.emit(): Yukarıda uydurduğumuz özelsquashedsinyalini etrafa (“Ben ezildim!”) diye bağırarak yayar.queue_free(): Görevini başarıyla tamamlayan düşmanımız, kendini oyun sahnesinden siler ve yok olur.
Bu sayede düşman ezildiğinde hem kendini silecek hem de (ileride skoru artırmak için kullanacağımız) bir sinyal yayacaktır.
Konuyla ilgili Youtube videosu aşağıdadır…
Bölüm Özeti
F6 tuşu ile oyununuzu çalıştırdığınızda, artık her yarım saniyede bir ekranın kenarlarından rastgele düşmanların belirdiğini ve karakterinize doğru yöneldiğini göreceksiniz!
Şu an karakterimiz düşmanların içinden geçiyor. Bir sonraki bölümümüzde oyuncumuzun da ölebilmesini sağlayacağız.