Godot Engine Oyun Mekanikleri - Bölüm 2: Candy Blast — Grid Veri Yapısı ve Şekerleri Yerleştirme
Bu bölümde oyun tahtasının arkasındaki veri yapısını oluşturacak, şekerleri rastgele yerleştirecek ve başlangıçta eşleşme oluşmamasını sağlayacağız. Bölüm sonunda grid üzerinde rastgele renkli şekerlerin göründüğünü göreceksiniz.
2.1 — Oyun Scriptini Oluşturma
Godot’da düğümlere davranış kazandırmak için script (kod dosyası) eklenir. Godot’un kendi dili olan GDScript kullanacağız. Python’a çok benzer, öğrenmesi kolaydır.
Adımlar:
Sol panelde Game düğümünü seçin
Inspector panelinin üstündeki küçük kağıt ikonuna (Attach Script) tıklayın — veya sağ tıklayıp Attach Script seçin
Açılan pencerede ayarları şu şekilde bırakın:
Language: GDScript
Path:res://game.gd
Create butonuna basın
Godot otomatik olarak script editörünü açacak ve şöyle bir şablon gösterecek:
1
extendsNode2D
Bu satır “Bu script bir Node2D düğümüne bağlıdır” demektir. Şimdi bu dosyanın tüm içeriğini silip aşağıdaki kodu yazacağız.
2.2 — Sabit Değerleri Tanımlama
game.gd dosyasının içeriğini tamamen silip şunu yazın:
1
2
3
4
5
6
7
8
9
10
11
12
extendsNode2D# --- Sabitler ---constGRID_SIZE:=8# Satır ve sütun sayısı (8x8)constCELL_SIZE:=64.0# Hücreler arası mesafe (512 / 8)constCANDY_SCALE:=0.63# Şeker görsellerini hücreye sığdırma oranı (grid çizgileri alan kaplar)# Grid'in ekrandaki başlangıç pozisyonu (sol üst köşe)constGRID_OFFSET:=Vector2(24,225)# Şeker türleri — her biri bir PNG dosyasına karşılık gelirconstCANDY_TYPES:=["red","yellow","blue","green","purple"]
Satır satır açıklama:
const GRID_SIZE := 8 — Tahtamız 8x8 olacak. const demek bu değer asla değişmez.
const CELL_SIZE := 64.0 — Grid görseli 512px, 8’e bölünce her hücre 64px. 64.0 yazıyoruz çünkü ileride piksel hesaplamalarında ondalıklı sonuçlar gerekebilir (GDScript’te 64 / 2 tam sayı bölmesi yapar, 64.0 / 2 ondalıklı sonuç verir).
const CANDY_SCALE := 0.63 — Şeker görselleri 82x82px ama grid çizgileri hücre içi alanı daraltır. 82×0.63 ≈ 52px ile şekerler hücrelere güzel oturur ve çizgilerle örtüşmez.
const GRID_OFFSET := Vector2(24, 225) — Grid’in sol üst köşesinin ekrandaki piksel konumu. Grid sprite’ı merkezden konumlandırıldığı için (288, 480), bu değer grid çizgilerinin kalınlığı hesaba katılarak ayarlanmıştır. Dikeyde 225, yatayda 24 ile şekerler hücrelere tam oturur.
const CANDY_TYPES — 5 renk şekerimiz var. Bu dizi ileride rastgele seçim ve dosya yolu oluşturmak için kullanılacak.
2.3 — Grid Veri Yapısı ve Değişkenler
Sabitlerden hemen sonra şu değişkenleri ekleyin:
1
2
3
4
# --- Değişkenler ---vargrid:=[]# 8x8 dizi — her hücrede şeker türü (string) tutacakvarcandy_sprites:=[]# 8x8 dizi — her hücredeki Sprite2D düğümünü tutacakvarcandy_textures:={}# Şeker ismi → Texture2D eşlemesi (önbellek)
Ne işe yarıyorlar?
grid — Oyunun mantıksal tahtası. Bir 2D dizi (dizi içinde dizi). Örneğin grid[2][5] → 2. satır, 5. sütundaki şekerin türü (“red”, “blue” vb.).
candy_sprites — Her hücredeki görsel düğümü tutan 2D dizi. Şekeri silmek veya hareket ettirmek istediğimizde bu referansı kullanacağız.
candy_textures — PNG dosyalarını her seferinde diskten okumamak için bir kez yükleyip bu sözlükte saklayacağız.
2.4 — Texture’ları Önceden Yükleme
Değişkenlerden sonra _ready() fonksiyonunu yazalım. Bu fonksiyon Godot’da özel bir fonksiyondur — düğüm sahneye eklendiğinde otomatik olarak bir kez çalışır.
_ready() → Godot’un yerleşik yaşam döngüsü (lifecycle) fonksiyonudur. Düğüm sahne ağacına eklendiğinde Godot bu fonksiyonu otomatik olarak bir kez çağırır. Yani oyun başladığında bu fonksiyon çalışır.
-> void → Bu fonksiyon geriye hiçbir değer döndürmez. GDScript’te void dönüş tipi “bu fonksiyon bir iş yapar ama sonuç üretmez” demektir.
1
2
3
_load_textures()
_init_grid()
_draw_candies()
Üç fonksiyonu sırasıyla çağırıyoruz. Sıra önemlidir: önce görsel dosyaları yükle, sonra mantıksal tahtayı oluştur, en son görselleri ekrana yerleştir. Eğer sıra değişirse, örneğin texture yüklenmeden çizim yapılırsa hata alırsınız.
Fonksiyon isimlerinin başındaki _ alt çizgi, Godot’da “bu fonksiyon dışarıdan değil, sadece bu script içinden çağrılır” anlamına gelen bir gelenektir (convention). Zorunlu değildir ama tüm Godot topluluğu bu kurala uyar.
_load_textures() — Satır satır açıklama:
1
func _load_textures() -> void:
Texture (görsel dosyası) yükleme fonksiyonumuz. Oyun başında bir kez çağrılır.
1
for candy_name in CANDY_TYPES:
CANDY_TYPES dizisi üzerinde döngü kurar. Sırasıyla candy_name değişkeni "red", "yellow", "blue", "green", "purple" değerlerini alır.
1
var path: String = "res://assets/images/" + candy_name + ".png"
Dosya yolunu string birleştirme ile oluşturuyoruz. Örneğin candy_name = "red" ise → "res://assets/images/red.png".
var path: String — Godot 4.6’da := operatörü string birleştirme sonucunun tipini her zaman çıkaramadığı için tip bilgisini açıkça yazıyoruz. Bu Godot 4.6’ya özgü bir gerekliliktir.
res:// → Godot’un proje kök dizinini temsil eden sanal yoldur. Gerçek dosya sistemi yolu yerine bu sanal yolu kullanırız, böylece oyun hangi platformda çalışırsa çalışsın doğru dosyayı bulur.
1
candy_textures[candy_name] = load(path)
load() → Godot’un yerleşik fonksiyonudur. Verilen yoldaki dosyayı diskten okuyup bellekte bir Resource nesnesine dönüştürür. PNG dosyaları için bu bir Texture2D nesnesi olur.
candy_textures[candy_name] → Sözlüğe (Dictionary) yeni bir anahtar-değer çifti ekler. Örneğin candy_textures["red"] artık red.png dosyasının texture’ını tutar.
Neden önceden yüklüyoruz? Her şekeri çizerken load() çağırmak yerine bir kez yükleyip sözlükte saklamak çok daha performanslıdır. 64 şeker × her kare = sürekli disk okuma yerine, 5 kez yükle + sözlükten oku.
2.5 — Grid’i Rastgele Doldurma (Eşleşmesiz)
Şimdi en önemli kısım: tahtayı rastgele şekerlerle dolduracağız ama başlangıçta hiçbir yerde 3’lü eşleşme olmamasını garanti edeceğiz.
func_init_grid()->void:# 8x8 boş grid oluşturgrid.clear()forrowinGRID_SIZE:vargrid_row:=[]forcolinGRID_SIZE:grid_row.append("")grid.append(grid_row)# Her hücreye eşleşme oluşturmayan rastgele şeker yerleştirforrowinGRID_SIZE:forcolinGRID_SIZE:varavailable:=CANDY_TYPES.duplicate()# Solda 2 aynı renk varsa o rengi çıkarifcol>=2andgrid[row][col-1]==grid[row][col-2]:available.erase(grid[row][col-1])# Üstte 2 aynı renk varsa o rengi çıkarifrow>=2andgrid[row-1][col]==grid[row-2][col]:available.erase(grid[row-1][col])grid[row][col]=available[randi()%available.size()]
Satır satır açıklama:
1
func _init_grid() -> void:
Grid’i sıfırdan oluşturan fonksiyon. Oyun başında ve her seviye geçişinde çağrılır.
1
grid.clear()
grid dizisinin içini tamamen boşaltır. Seviye geçişlerinde eski grid verisini temizlemek için gerekli. İlk çalıştırmada zaten boş ama temizlemek güvenli bir alışkanlıktır.
1
2
3
4
5
for row in GRID_SIZE:
var grid_row := []
for col in GRID_SIZE:
grid_row.append("")
grid.append(grid_row)
for row in GRID_SIZE → row değişkeni 0’dan 7’ye kadar (toplam 8) döner. GDScript’te for i in N yazarsak, i 0, 1, 2, …, N-1 değerlerini alır.
Her satır için boş bir grid_row dizisi oluşturuyoruz.
İç döngüde her sütun için boş string "" ekliyoruz.
Sonuçta grid şöyle bir yapıya sahip olur: [["","","",...""], ["","","",...""], ...] — 8 satır, her satırda 8 boş hücre.
1
2
for row in GRID_SIZE:
for col in GRID_SIZE:
İkinci geçişte tüm hücreleri sol üstten (0,0) sağ alta (7,7) doğru dolduruyoruz. Bu sıra önemlidir çünkü kontrol ettiğimiz sol ve üst hücreler zaten dolu olmalı.
1
var available := CANDY_TYPES.duplicate()
CANDY_TYPES.duplicate() → Orijinal dizinin bir kopyasını oluşturur: ["red", "yellow", "blue", "green", "purple"]. Kopyasını almamız şart çünkü aşağıda bu diziden eleman çıkaracağız — orijinali değiştirmek istemeyiz.
1
2
if col >= 2 and grid[row][col - 1] == grid[row][col - 2]:
available.erase(grid[row][col - 1])
Yatay kontrol:col >= 2 → En az 2 sütun geride bakabilecek kadar ilerdeyiz mi? (0. ve 1. sütunlarda sol tarafta 2 hücre yok).
grid[row][col - 1] == grid[row][col - 2] → Soldaki iki hücre aynı renk mi? Örneğin ikisi de "red" ise, bu hücreye de "red" koyarsak 3’lü eşleşme olur.
available.erase(grid[row][col - 1]) → O rengi kullanılabilir listesinden çıkar. erase() diziden belirtilen elemanı siler. Artık bu renk seçilemez.
1
2
if row >= 2 and grid[row - 1][col] == grid[row - 2][col]:
available.erase(grid[row - 1][col])
Dikey kontrol: Aynı mantık ama bu sefer üstteki iki hücreye bakıyoruz. row - 1 bir üst satır, row - 2 iki üst satır.
randi() → Godot’un yerleşik rastgele tam sayı üreten fonksiyonu.
% available.size() → Modülo (kalan) operatörü ile sonucu 0 ile available.size() - 1 arasına sınırlıyoruz. Örneğin listede 4 eleman kaldıysa, randi() % 4 → 0, 1, 2 veya 3 döner.
available[...] → Listeden rastgele bir renk seçilir ve grid’e yazılır.
Neden available listesi her zaman en az 1 eleman içerir? 5 renkten en kötü durumda hem yatayda hem dikeyde birer renk çıkarılır = 5 - 2 = 3 renk kalır. Yani her zaman en az 3 seçenek mevcuttur ve program asla boş listeden seçim yapmak zorunda kalmaz.
2.6 — Şekerleri Ekrana Çizme
Şimdi grid dizisindeki verileri gerçek görsellere dönüştürelim:
func_draw_candies()->void:# Önceki sprite'ları temizlecandy_sprites.clear()forchildinget_children():ifchild.name!="Grid":child.queue_free()# Her hücre için sprite oluşturforrowinGRID_SIZE:varsprite_row:=[]forcolinGRID_SIZE:varcandy_type:String=grid[row][col]ifcandy_type=="":sprite_row.append(null)continuevarsprite:=Sprite2D.new()sprite.texture=candy_textures[candy_type]sprite.scale=Vector2(CANDY_SCALE,CANDY_SCALE)sprite.position=_grid_to_pixel(row,col)add_child(sprite)sprite_row.append(sprite)candy_sprites.append(sprite_row)
Satır satır açıklama:
1
candy_sprites.clear()
Önceki görsel referanslarını temizliyoruz. Seviye geçişlerinde eski sprite referanslarının kalmamasını sağlar.
1
2
3
for child in get_children():
if child.name != "Grid":
child.queue_free()
get_children() → Game düğümünün altındaki tüm çocuk düğümleri bir dizi olarak döndürür (Grid sprite’ı, eski şeker sprite’ları vs.).
child.name != "Grid" → Grid arka plan görseli hariç tüm çocukları siliyoruz. Grid’i silmemeliyiz çünkü o tahtanın arka plan çizgilerini gösterir.
queue_free() → Düğümü sahne ağacından güvenli şekilde kaldırır. “Güvenli” demek: Godot mevcut frame’i bitirdikten sonra siler, böylece silme işlemi sırasında hata oluşmaz. free() yerine queue_free() kullanmak Godot’ta standart uygulamadır.
1
2
for row in GRID_SIZE:
var sprite_row := []
Her satır için boş bir sprite referans dizisi oluşturuyoruz. Bu dizi o satırdaki sprite’ları tutacak.
1
2
3
4
5
for col in GRID_SIZE:
var candy_type: String = grid[row][col]
if candy_type == "":
sprite_row.append(null)
continue
grid[row][col] → Mantıksal grid’den bu hücrenin şeker türünü okuyoruz (örneğin "red").
Eğer hücre boşsa ("") → sprite listesine null ekliyoruz (bu hücrede görsel yok) ve continue ile döngünün sonraki iterasyonuna atlıyoruz. İlk oluşturmada boş hücre olmaz ama ileride yerçekimi sonrası boş hücreler oluşacak.
1
var sprite := Sprite2D.new()
Sprite2D.new() → Kod ile yeni bir Sprite2D düğümü oluşturur. Godot Editor’da “Add Node” ile yaptığımız işlemin aynısını kodda yapıyoruz. Bu düğüm henüz sahneye eklenmedi, sadece bellekte var.
1
sprite.texture = candy_textures[candy_type]
Daha önce _load_textures() ile yüklediğimiz texture’ı sprite’a atıyoruz. Örneğin candy_type = "blue" ise → candy_textures["blue"] = blue.png‘nin texture’ı.
1
sprite.scale = Vector2(CANDY_SCALE, CANDY_SCALE)
Sprite’ın hem x hem y ekseninde ölçeğini 0.63 yapıyoruz. 82px × 0.63 ≈ 52px ile şeker, 64px’lik hücrenin içine güzel oturur ve grid çizgilerinin üstüne taşmaz.
1
sprite.position = _grid_to_pixel(row, col)
Grid koordinatlarını (satır, sütun) ekran piksellerine çeviriyoruz. Bu fonksiyonu bir sonraki bölümde yazacağız.
add_child(sprite) → Sprite’ı Game düğümünün çocuğu olarak sahne ağacına ekler. Bu adımdan sonra sprite ekranda görünür hale gelir. Godot’ta bir düğüm sahne ağacına eklenmeden ekranda görünmez.
candy_sprites.append(sprite_row) → Tamamlanan satırı ana diziye ekliyoruz. Sonuçta candy_sprites[row][col] ile herhangi bir hücrenin sprite’ına erişebiliyoruz.
2.7 — Hücre Pozisyonu Hesaplama
Grid koordinatlarını (satır, sütun) ekran piksellerine çeviren yardımcı fonksiyon: