Godot Engine Oyun Mekanikleri - Bölüm 5: Candy Blast — Yerçekimi, Doldurma ve Zincir Reaksiyon
Godot Match-3 oyununda yerçekimi, boş hücre doldurma ve zincir reaksiyon mekaniği. Düşerek dolan şekerlerin Tween animasyonu.
Bu bölümde eşleşme sonrası boşalan hücrelere yerçekimi uygulayacak, çapraz kayma mekaniği ekleyecek, yeni şekerlerle doldurma yapacak ve zincir reaksiyonları yöneteceğiz. Bölüm sonunda oyun gerçek bir Match-3 gibi çalışacak.
5.1 — Genel Akış
Eşleşme sonrası akış şöyle olacak:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Eşleşme bulundu → Şekerler silindi → Boşluklar oluştu
│
▼
Dikey yerçekimi (şekerler aşağı düşer)
│
▼
Çapraz kayma (boşluk doldurulamıyorsa yanlara kayarak dolar)
│
▼
Üstten yeni şekerler gelir
│
▼
Animasyon oynar
│
▼
Tekrar eşleşme kontrolü (zincir reaksiyon)
│
├── Eşleşme var → Başa dön
└── Eşleşme yok → Oyuncunun sırası
Bu bölümde 6 yeni fonksiyon yazacağız ve 1 mevcut fonksiyonu güncelleyeceğiz.
5.2 — Dikey Yerçekimi
Her sütunda şekerleri aşağı çekip boşlukları üste toplayan fonksiyon. Mantık basit: her sütunu aşağıdan yukarı tarayıp, dolu hücreleri alta doğru sıkıştırıyoruz.
_reverse_swap() fonksiyonunun altına ekleyin:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func _apply_vertical_gravity() -> bool:
var moved := false
for col in GRID_SIZE:
var write_row := GRID_SIZE - 1 # Yazma pozisyonu (alttan başla)
for read_row in range(GRID_SIZE - 1, -1, -1): # Alttan üste tara
if grid[read_row][col] != "":
if read_row != write_row:
# Şekeri aşağı taşı (veri)
grid[write_row][col] = grid[read_row][col]
grid[read_row][col] = ""
# Sprite referansını taşı
candy_sprites[write_row][col] = candy_sprites[read_row][col]
candy_sprites[read_row][col] = null
moved = true
write_row -= 1
return moved
Nasıl çalışıyor? Adım adım bir sütun örneği:
1
2
3
4
5
6
7
8
9
10
11
12
13
Başlangıç: [__, 🔴, __, 🟢, __, 🔵, __, 🟡] (row 0-7, __ = boş)
write_row = 7
read_row = 7: 🟡 bulundu, read==write → write_row = 6
read_row = 6: boş → geç
read_row = 5: 🔵 bulundu, 5 ≠ 6 → 🔵'yu row 6'ya taşı → write_row = 5
read_row = 4: boş → geç
read_row = 3: 🟢 bulundu, 3 ≠ 5 → 🟢'yu row 5'e taşı → write_row = 4
read_row = 2: boş → geç
read_row = 1: 🔴 bulundu, 1 ≠ 4 → 🔴'yu row 4'e taşı → write_row = 3
read_row = 0: boş → geç
Sonuç: [__, __, __, __, 🔴, 🟢, 🔵, 🟡] ← boşluklar üstte
Satır satır açıklama:
1
2
func _apply_vertical_gravity() -> bool:
var moved := false
-> bool→ Herhangi bir şeker taşındıysatrue, hiçbir şey değişmediysefalsedöner. Bu bilgi_settle_candies()tarafından döngü kontrolü için kullanılacak.
1
2
for col in GRID_SIZE:
var write_row := GRID_SIZE - 1
- Her sütunu ayrı ayrı işliyoruz.
write_rowbir yazma imleci görevi görür — sütunun en altından başlar (row 7) ve her dolu hücre yerleştirildiğinde bir yukarı çıkar.
1
for read_row in range(GRID_SIZE - 1, -1, -1):
range(7, -1, -1)→ 7, 6, 5, 4, 3, 2, 1, 0 — alttan yukarıya doğru okuyoruz. Bu iki-işaretçi (two-pointer) algoritmasıdır:read_rowokur,write_rowyazar.
1
2
if grid[read_row][col] != "":
if read_row != write_row:
- Dolu hücre bulduğumuzda: eğer okuma ve yazma pozisyonu farklıysa → şeker taşınmalı. Aynıysa → şeker zaten doğru yerde.
1
2
3
4
5
grid[write_row][col] = grid[read_row][col]
grid[read_row][col] = ""
candy_sprites[write_row][col] = candy_sprites[read_row][col]
candy_sprites[read_row][col] = null
moved = true
- Hem
gridverisini hemcandy_spritesreferansını yeni pozisyona taşıyoruz. Eski pozisyonu boşaltıyoruz. Sprite’ın ekrandaki pozisyonu henüz değişmez — o animasyonla yapılacak.
1
write_row -= 1
- Yazma imlecini bir yukarı kaydırıyoruz. Sonraki dolu hücre buraya yazılacak.
5.3 — Çapraz Kayma
Dikey yerçekiminden sonra hâlâ boşluk kalabilir. Özellikle bir sütunun tamamı boşaldığında, yanındaki şekerlerin oraya “kayarak düşmesi” gerekir.
Kural: Bir hücre boşsa VE doğrudan üstü de boşsa (yani dikey yerçekimi dolduramıyorsa), çapraz yukarıdan (sol-üst veya sağ-üst) bir şekeri bu hücreye kaydır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func _apply_diagonal_slide() -> bool:
for row in range(GRID_SIZE - 1, 0, -1): # Alttan üste (row 0 hariç)
for col in GRID_SIZE:
if grid[row][col] != "":
continue # Dolu hücre, atla
# Üstü de boşsa dikey yerçekimi işe yaramaz → çapraz dene
if grid[row - 1][col] != "":
continue # Üstte şeker var, dikey yerçekimi halleder
# Sol üstten kayma
if col > 0 and grid[row - 1][col - 1] != "":
grid[row][col] = grid[row - 1][col - 1]
grid[row - 1][col - 1] = ""
candy_sprites[row][col] = candy_sprites[row - 1][col - 1]
candy_sprites[row - 1][col - 1] = null
return true # Bir kayma yaptık, başa dön
# Sağ üstten kayma
if col < GRID_SIZE - 1 and grid[row - 1][col + 1] != "":
grid[row][col] = grid[row - 1][col + 1]
grid[row - 1][col + 1] = ""
candy_sprites[row][col] = candy_sprites[row - 1][col + 1]
candy_sprites[row - 1][col + 1] = null
return true
return false # Hiç kayma olmadı
Neden her seferinde sadece 1 kayma yapıyoruz?
Bir şeker kaydığında, kaynak hücre boşalır. Bu boşluk üstteki şekerlerin düşmesine yol açabilir. O yüzden her kaymadan sonra tekrar dikey yerçekimi uygulamamız gerekir. return true ile fonksiyondan çıkıp döngüyü yeniden başlatıyoruz.
Neden grid[row-1][col] != "" kontrolü var?
Eğer boş hücrenin hemen üstünde bir şeker varsa, dikey yerçekimi o şekeri zaten aşağı düşürecektir. Çapraz kaymaya gerek yok. Çapraz kayma sadece dikey yolun tıkalı olduğu durumlarda devreye girer.
Görsel örnek:
1
2
3
4
5
6
7
8
Yerçekimi sonrası: Çapraz kayma sonrası:
🔴 __ __ __ __ __
🟢 __ __ → 🔴 __ __
🔵 __ __ 🟢 🔵 __
🔵, (2,0)'dan (2,1)'e çapraz kaydı.
Sonra tekrar yerçekimi: 🔵 (2,1)'den düşecek yer yok, kalır.
Ama 🔴 ve 🟢 de bir satır düşer.
5.4 — Yerleşme Döngüsü
Dikey yerçekimi ve çapraz kaymayı birleştiren fonksiyon. İkisini stabil olana kadar tekrar eder.
1
2
3
4
5
6
func _settle_candies() -> void:
var changed := true
while changed:
changed = _apply_vertical_gravity()
if not changed:
changed = _apply_diagonal_slide()
Satır satır açıklama:
1
2
func _settle_candies() -> void:
var changed := true
changeddöngü kontrolü için kullanılır.trueile başlıyoruz ki döngüye en az bir kez girelim.
1
2
while changed:
changed = _apply_vertical_gravity()
- Önce dikey yerçekimi uygula. Hareket olduysa
changed = true, olmadıysafalse.
1
2
if not changed:
changed = _apply_diagonal_slide()
- Dikey yerçekimi bir şey taşımadıysa → çapraz kaymayı dene. Kayma olduysa
changed = trueolur ve döngü başa döner (tekrar dikey yerçekimi). Kayma da olmadıysachanged = falsekalır ve döngü biter.
Akış:
1
2
3
4
5
6
┌─→ Dikey yerçekimi uygula
│ ├── Hareket oldu → Tekrar dikey yerçekimi
│ └── Hareket olmadı → Çapraz kayma dene
│ ├── Kayma oldu → Başa dön (tekrar dikey yerçekimi)
│ └── Kayma olmadı → Stabil! Döngüden çık
└──────────┘
5.5 — Boş Hücreleri Yeni Şekerlerle Doldurma
Yerleşme sonrası boş kalan hücreler (her sütunun üst kısmında) yeni rastgele şekerlerle doldurulur. Yeni şekerler grid’in üstünden başlayıp animasyonla düşecek.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func _fill_empty_cells() -> void:
for col in GRID_SIZE:
# Bu sütundaki boş hücre sayısını bul (hepsi üstte olmalı)
var empty_count := 0
for row in GRID_SIZE:
if grid[row][col] == "":
empty_count += 1
else:
break # Yerçekimi sonrası boşluklar sadece üstte
# Boş hücreleri doldur
for i in empty_count:
var candy_type: String = CANDY_TYPES[randi() % CANDY_TYPES.size()]
grid[i][col] = candy_type
var sprite := Sprite2D.new()
sprite.texture = candy_textures[candy_type]
sprite.scale = Vector2(CANDY_SCALE, CANDY_SCALE)
# Grid'in üstünde başlat (animasyonla düşecek)
sprite.position = _grid_to_pixel(i - empty_count, col)
sprite.modulate.a = 0.0 # Başlangıçta görünmez
add_child(sprite)
candy_sprites[i][col] = sprite
Satır satır açıklama:
1
2
func _fill_empty_cells() -> void:
for col in GRID_SIZE:
- Her sütunu ayrı ayrı işliyoruz. Yerçekimi sonrası boş hücreler her sütunun üst kısmında toplanmış durumda.
1
2
3
4
5
6
var empty_count := 0
for row in GRID_SIZE:
if grid[row][col] == "":
empty_count += 1
else:
break
- Sütunun tepesinden aşağı doğru boş hücreleri sayıyoruz. İlk dolu hücreye ulaşınca
breakile çıkıyoruz. Yerçekimi zaten boşlukları üste topladığı için ortada veya altta boş hücre kalmaz.
1
2
3
for i in empty_count:
var candy_type: String = CANDY_TYPES[randi() % CANDY_TYPES.size()]
grid[i][col] = candy_type
for i in empty_count→ 0’danempty_count - 1‘e kadar döner. Her boş hücre için rastgele bir şeker türü seçipgrid‘e yazıyoruz. Burada başlangıç eşleşme kontrolü yapmıyoruz — yeni gelen şekerler eşleşme oluşturursa zincir reaksiyon halledecek.
1
2
3
var sprite := Sprite2D.new()
sprite.texture = candy_textures[candy_type]
sprite.scale = Vector2(CANDY_SCALE, CANDY_SCALE)
- Yeni sprite oluşturup texture ve ölçeği ayarlıyoruz (Bölüm 2’deki
_draw_candies()ile aynı mantık).
1
sprite.position = _grid_to_pixel(i - empty_count, col)
- Kritik satır: Sprite’ı grid’in üstünde konumlandırıyoruz.
i - empty_countnegatif bir satır numarası verir. Örneğin 3 boş hücre varsa:i=0→_grid_to_pixel(-3, col)→ grid’in 3 hücre üstü. Animasyon sırasında sprite buradan aşağı düşerek gerçek yerine (row 0) gelecek.
1
sprite.modulate.a = 0.0
- Sprite’ı tamamen görünmez başlatıyoruz.
modulate.aalfa (saydamlık) kanalıdır:0.0= tamamen şeffaf,1.0= tamamen opak. Grid dışında oluştuğu için oyuncuya görünmemeli,_animate_board()içinde yavaşça görünür olacak.
1
2
add_child(sprite)
candy_sprites[i][col] = sprite
- Sprite’ı sahneye ekleyip referansını diziye kaydediyoruz.
5.6 — Tahta Animasyonu
Tüm sprite’ları olması gereken pozisyona animasyonla taşıyan fonksiyon. Yerçekimi ve doldurma veriyi anında değiştirir ama görseller eski yerlerinde kalır. Bu fonksiyon görselleri doğru pozisyona animasyonla kaydırır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func _animate_board(callback: Callable) -> void:
var tween := create_tween()
tween.set_parallel(true)
var has_animation := false
for row in GRID_SIZE:
for col in GRID_SIZE:
var sprite: Sprite2D = candy_sprites[row][col]
if sprite == null:
continue
var target := _grid_to_pixel(row, col)
if not sprite.position.is_equal_approx(target):
tween.tween_property(sprite, "position", target, 0.3) \
.set_ease(Tween.EASE_IN) \
.set_trans(Tween.TRANS_QUAD)
has_animation = true
# Görünmez sprite'ları grid alanına girerken görünür yap
if sprite.modulate.a < 1.0:
tween.tween_property(sprite, "modulate:a", 1.0, 0.15)
has_animation = true
if has_animation:
tween.set_parallel(false)
tween.tween_callback(callback)
else:
callback.call()
Satır satır açıklama:
1
func _animate_board(callback: Callable) -> void:
callback: Callable→ Animasyon bitince çağrılacak fonksiyonu parametre olarak alır. Bu callback deseni sayesinde aynı fonksiyonu farklı devam işlemleriyle kullanabiliriz. Bizim kullanımımızda callback =_check_chain_matches.
1
2
var tween := create_tween()
tween.set_parallel(true)
- Yeni tween oluşturup paralel moda alıyoruz. Tüm sprite animasyonları aynı anda çalışacak.
1
2
3
4
5
6
var has_animation := false
for row in GRID_SIZE:
for col in GRID_SIZE:
var sprite: Sprite2D = candy_sprites[row][col]
if sprite == null:
continue
- Tüm hücreleri tarıyoruz.
nullolan hücreler (boş) atlanır.
1
2
3
4
5
6
var target := _grid_to_pixel(row, col)
if not sprite.position.is_equal_approx(target):
tween.tween_property(sprite, "position", target, 0.3) \
.set_ease(Tween.EASE_IN) \
.set_trans(Tween.TRANS_QUAD)
has_animation = true
target→ Sprite’ın olması gereken piksel pozisyonu (grid verisine göre).is_equal_approx()→ İkiVector2değerini karşılaştırır. Float sayılarda küçük yuvarlama hataları olabileceği için==yerine bu fonksiyon kullanılır.- Pozisyon farklıysa → Tween ile animasyonlu taşıma ekler.
0.3saniye sürer. EASE_IN+TRANS_QUAD→ Hareket başta yavaş, sona doğru hızlanır. Bu, gerçek yerçekimini simüle eder — düşen bir nesne giderek hızlanır.
1
2
3
if sprite.modulate.a < 1.0:
tween.tween_property(sprite, "modulate:a", 1.0, 0.15)
has_animation = true
- Yeni oluşturulan sprite’lar
modulate.a = 0.0(görünmez) başlatılmıştı. Bu kontrol onları 0.15 saniyede tamamen görünür yapar. "modulate:a"→ Godot’ta alt özellik yolu.modulateözelliğinina(alfa) kanalını anime eder.
1
2
3
4
5
if has_animation:
tween.set_parallel(false)
tween.tween_callback(callback)
else:
callback.call()
- Animasyon varsa → paralel modu kapat, tüm animasyonlar bittikten sonra
callbackçağrılsın. - Animasyon yoksa (hiçbir sprite taşınmadı) →
callback.call()ile hemen devam et. Beklemeye gerek yok.
5.7 — Yerçekimi + Doldurma Orkestratörü
Tüm adımları sırayla çalıştıran ana fonksiyon:
1
2
3
4
func _apply_gravity_and_fill() -> void:
_settle_candies()
_fill_empty_cells()
_animate_board(_check_chain_matches)
Satır satır açıklama:
1
2
func _apply_gravity_and_fill() -> void:
_settle_candies()
- Önce mevcut şekerleri yerleştirir (dikey yerçekimi + çapraz kayma döngüsü). Tüm dolu hücreler mümkün olan en alt pozisyona taşınır.
1
_fill_empty_cells()
- Üstte kalan boş hücrelere yeni rastgele şekerler yerleştirir. Sprite’lar grid’in üstünde, görünmez başlar.
1
_animate_board(_check_chain_matches)
- Tüm veri değişikliklerini animasyonla görselleştirir. Animasyon bitince
_check_chain_matchescallback’i çağrılır — bu da zincir reaksiyon döngüsünü başlatır.
5.8 — Zincir Reaksiyon
Yerçekimi ve doldurma sonrası yeni eşleşmeler oluşabilir. Bu kontrolü yapan fonksiyon:
1
2
3
4
5
6
7
func _check_chain_matches() -> void:
var matches := _find_matches()
if matches.size() > 0:
_remove_matches(matches)
_apply_gravity_and_fill() # Tekrar yerçekimi + doldurma + kontrol
else:
is_animating = false # Zincir bitti, oyuncunun sırası
Satır satır açıklama:
1
2
func _check_chain_matches() -> void:
var matches := _find_matches()
- Yerçekimi ve doldurma sonrası tahtada yeni eşleşme var mı kontrol ediyoruz.
1
2
3
if matches.size() > 0:
_remove_matches(matches)
_apply_gravity_and_fill()
- Yeni eşleşme bulunduysa → sil + tekrar yerçekimi + doldurma + animasyon + tekrar kontrol. Bu öz-yinelemeli (recursive) bir zincir oluşturur:
_apply_gravity_and_fill()bitince tekrar_check_chain_matches()çağrılır.
1
2
else:
is_animating = false
- Eşleşme yoksa → zincir bitti!
is_animating = falseile oyuncunun tekrar tıklamasına izin veriyoruz.
Zincir akışı:
1
2
3
Eşleşme bul → Sil → Yerçekimi → Doldur → Animasyon →
→ Eşleşme bul → Sil → Yerçekimi → Doldur → Animasyon →
→ Eşleşme yok → DUR
Her döngü bir “zincir” sayılır. Oyuncular büyük zincirler oluşturmaya bayılır!
5.9 — _on_swap_finished Güncelleme
Mevcut _on_swap_finished() fonksiyonunu güncelleyin. Eşleşme bulununca artık yerçekimi akışını başlatacak:
Mevcut _on_swap_finished() fonksiyonunu şununla değiştirin:
1
2
3
4
5
6
7
func _on_swap_finished() -> void:
var matches := _find_matches()
if matches.size() > 0:
_remove_matches(matches)
_apply_gravity_and_fill()
else:
_reverse_swap()
Bu fonksiyon Bölüm 4’teki ile neredeyse aynı. Tek fark:
is_animating = falseyerine_apply_gravity_and_fill()çağırıyoruz. Yerçekimi + doldurma + zincir döngüsünün sonunda_check_chain_matches()zatenis_animating = falseyapacak.
5.10 — Tam Kod (game.gd)
İşte game.gd dosyasının bu bölüm sonundaki tam hali:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
extends Node2D
# --- Sabitler ---
const GRID_SIZE := 8
const CELL_SIZE := 64.0
const CANDY_SCALE := 0.63
const GRID_OFFSET := Vector2(24, 225)
const CANDY_TYPES := ["red", "yellow", "blue", "green", "purple"]
# --- Değişkenler ---
var grid := []
var candy_sprites := []
var candy_textures := {}
var selected_cell := Vector2i(-1, -1)
var is_animating := false
var last_swap := [Vector2i(-1, -1), Vector2i(-1, -1)]
func _ready() -> void:
_load_textures()
_init_grid()
_draw_candies()
func _load_textures() -> void:
for candy_name in CANDY_TYPES:
var path: String = "res://assets/images/" + candy_name + ".png"
candy_textures[candy_name] = load(path)
func _init_grid() -> void:
grid.clear()
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:
for col in GRID_SIZE:
var available := CANDY_TYPES.duplicate()
if col >= 2 and grid[row][col - 1] == grid[row][col - 2]:
available.erase(grid[row][col - 1])
if row >= 2 and grid[row - 1][col] == grid[row - 2][col]:
available.erase(grid[row - 1][col])
grid[row][col] = available[randi() % available.size()]
func _draw_candies() -> void:
candy_sprites.clear()
for child in get_children():
if child.name != "Grid":
child.queue_free()
for row in GRID_SIZE:
var sprite_row := []
for col in GRID_SIZE:
var candy_type: String = grid[row][col]
if candy_type == "":
sprite_row.append(null)
continue
var sprite := 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)
func _grid_to_pixel(row: int, col: int) -> Vector2:
var x := GRID_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2
var y := GRID_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2
return Vector2(x, y)
func _pixel_to_grid(pixel: Vector2) -> Vector2i:
var col := int((pixel.x - GRID_OFFSET.x) / CELL_SIZE)
var row := int((pixel.y - GRID_OFFSET.y) / CELL_SIZE)
return Vector2i(row, col)
func _is_valid_cell(cell: Vector2i) -> bool:
return cell.x >= 0 and cell.x < GRID_SIZE and cell.y >= 0 and cell.y < GRID_SIZE
func _input(event: InputEvent) -> void:
if is_animating:
return
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
var cell := _pixel_to_grid(event.position)
if _is_valid_cell(cell):
_on_cell_clicked(cell)
func _on_cell_clicked(cell: Vector2i) -> void:
if grid[cell.x][cell.y] == "":
return
if selected_cell == Vector2i(-1, -1):
selected_cell = cell
_highlight_cell(cell, true)
return
if selected_cell == cell:
_highlight_cell(cell, false)
selected_cell = Vector2i(-1, -1)
return
if _is_adjacent(selected_cell, cell):
_highlight_cell(selected_cell, false)
_swap_candies(selected_cell, cell)
selected_cell = Vector2i(-1, -1)
else:
_highlight_cell(selected_cell, false)
selected_cell = cell
_highlight_cell(cell, true)
func _is_adjacent(cell_a: Vector2i, cell_b: Vector2i) -> bool:
var diff := (cell_a - cell_b).abs()
return (diff.x == 1 and diff.y == 0) or (diff.x == 0 and diff.y == 1)
func _highlight_cell(cell: Vector2i, highlight: bool) -> void:
var sprite: Sprite2D = candy_sprites[cell.x][cell.y]
if sprite == null:
return
if highlight:
sprite.scale = Vector2(CANDY_SCALE * 1.2, CANDY_SCALE * 1.2)
sprite.modulate = Color(1.2, 1.2, 1.2, 1.0)
else:
sprite.scale = Vector2(CANDY_SCALE, CANDY_SCALE)
sprite.modulate = Color(1.0, 1.0, 1.0, 1.0)
func _swap_candies(cell_a: Vector2i, cell_b: Vector2i) -> void:
is_animating = true
last_swap = [cell_a, cell_b]
var temp: String = grid[cell_a.x][cell_a.y]
grid[cell_a.x][cell_a.y] = grid[cell_b.x][cell_b.y]
grid[cell_b.x][cell_b.y] = temp
var sprite_a: Sprite2D = candy_sprites[cell_a.x][cell_a.y]
var sprite_b: Sprite2D = candy_sprites[cell_b.x][cell_b.y]
candy_sprites[cell_a.x][cell_a.y] = sprite_b
candy_sprites[cell_b.x][cell_b.y] = sprite_a
var pos_a := _grid_to_pixel(cell_a.x, cell_a.y)
var pos_b := _grid_to_pixel(cell_b.x, cell_b.y)
var tween := create_tween()
tween.set_parallel(true)
tween.tween_property(sprite_a, "position", pos_b, 0.2).set_ease(Tween.EASE_IN_OUT)
tween.tween_property(sprite_b, "position", pos_a, 0.2).set_ease(Tween.EASE_IN_OUT)
tween.set_parallel(false)
tween.tween_callback(_on_swap_finished)
func _on_swap_finished() -> void:
var matches := _find_matches()
if matches.size() > 0:
_remove_matches(matches)
_apply_gravity_and_fill()
else:
_reverse_swap()
func _find_matches() -> Array:
var matches := []
for row in GRID_SIZE:
var col := 0
while col < GRID_SIZE:
var candy_type: String = grid[row][col]
if candy_type == "":
col += 1
continue
var match_length := 1
while col + match_length < GRID_SIZE and grid[row][col + match_length] == candy_type:
match_length += 1
if match_length >= 3:
var match_group := []
for i in match_length:
match_group.append(Vector2i(row, col + i))
matches.append(match_group)
col += match_length
for col in GRID_SIZE:
var row := 0
while row < GRID_SIZE:
var candy_type: String = grid[row][col]
if candy_type == "":
row += 1
continue
var match_length := 1
while row + match_length < GRID_SIZE and grid[row + match_length][col] == candy_type:
match_length += 1
if match_length >= 3:
var match_group := []
for i in match_length:
match_group.append(Vector2i(row + i, col))
matches.append(match_group)
row += match_length
return matches
func _remove_matches(matches: Array) -> void:
var cells_to_remove := {}
for match_group in matches:
for cell in match_group:
cells_to_remove[cell] = true
for cell: Vector2i in cells_to_remove:
var sprite: Sprite2D = candy_sprites[cell.x][cell.y]
if sprite != null:
sprite.queue_free()
candy_sprites[cell.x][cell.y] = null
grid[cell.x][cell.y] = ""
func _reverse_swap() -> void:
var cell_a: Vector2i = last_swap[0]
var cell_b: Vector2i = last_swap[1]
var temp: String = grid[cell_a.x][cell_a.y]
grid[cell_a.x][cell_a.y] = grid[cell_b.x][cell_b.y]
grid[cell_b.x][cell_b.y] = temp
var sprite_a: Sprite2D = candy_sprites[cell_a.x][cell_a.y]
var sprite_b: Sprite2D = candy_sprites[cell_b.x][cell_b.y]
candy_sprites[cell_a.x][cell_a.y] = sprite_b
candy_sprites[cell_b.x][cell_b.y] = sprite_a
var pos_a := _grid_to_pixel(cell_a.x, cell_a.y)
var pos_b := _grid_to_pixel(cell_b.x, cell_b.y)
var tween := create_tween()
tween.set_parallel(true)
tween.tween_property(sprite_a, "position", pos_b, 0.2).set_ease(Tween.EASE_IN_OUT)
tween.tween_property(sprite_b, "position", pos_a, 0.2).set_ease(Tween.EASE_IN_OUT)
tween.set_parallel(false)
tween.tween_callback(func() -> void: is_animating = false)
# --- Yerçekimi ve Doldurma ---
func _apply_vertical_gravity() -> bool:
var moved := false
for col in GRID_SIZE:
var write_row := GRID_SIZE - 1
for read_row in range(GRID_SIZE - 1, -1, -1):
if grid[read_row][col] != "":
if read_row != write_row:
grid[write_row][col] = grid[read_row][col]
grid[read_row][col] = ""
candy_sprites[write_row][col] = candy_sprites[read_row][col]
candy_sprites[read_row][col] = null
moved = true
write_row -= 1
return moved
func _apply_diagonal_slide() -> bool:
for row in range(GRID_SIZE - 1, 0, -1):
for col in GRID_SIZE:
if grid[row][col] != "":
continue
if grid[row - 1][col] != "":
continue
if col > 0 and grid[row - 1][col - 1] != "":
grid[row][col] = grid[row - 1][col - 1]
grid[row - 1][col - 1] = ""
candy_sprites[row][col] = candy_sprites[row - 1][col - 1]
candy_sprites[row - 1][col - 1] = null
return true
if col < GRID_SIZE - 1 and grid[row - 1][col + 1] != "":
grid[row][col] = grid[row - 1][col + 1]
grid[row - 1][col + 1] = ""
candy_sprites[row][col] = candy_sprites[row - 1][col + 1]
candy_sprites[row - 1][col + 1] = null
return true
return false
func _settle_candies() -> void:
var changed := true
while changed:
changed = _apply_vertical_gravity()
if not changed:
changed = _apply_diagonal_slide()
func _fill_empty_cells() -> void:
for col in GRID_SIZE:
var empty_count := 0
for row in GRID_SIZE:
if grid[row][col] == "":
empty_count += 1
else:
break
for i in empty_count:
var candy_type: String = CANDY_TYPES[randi() % CANDY_TYPES.size()]
grid[i][col] = candy_type
var sprite := Sprite2D.new()
sprite.texture = candy_textures[candy_type]
sprite.scale = Vector2(CANDY_SCALE, CANDY_SCALE)
sprite.position = _grid_to_pixel(i - empty_count, col)
add_child(sprite)
candy_sprites[i][col] = sprite
func _animate_board(callback: Callable) -> void:
var tween := create_tween()
tween.set_parallel(true)
var has_animation := false
for row in GRID_SIZE:
for col in GRID_SIZE:
var sprite: Sprite2D = candy_sprites[row][col]
if sprite == null:
continue
var target := _grid_to_pixel(row, col)
if not sprite.position.is_equal_approx(target):
tween.tween_property(sprite, "position", target, 0.3) \
.set_ease(Tween.EASE_IN) \
.set_trans(Tween.TRANS_QUAD)
has_animation = true
# Görünmez sprite'ları grid alanına girerken görünür yap
if sprite.modulate.a < 1.0:
tween.tween_property(sprite, "modulate:a", 1.0, 0.15)
has_animation = true
if has_animation:
tween.set_parallel(false)
tween.tween_callback(callback)
else:
callback.call()
func _apply_gravity_and_fill() -> void:
_settle_candies()
_fill_empty_cells()
_animate_board(_check_chain_matches)
func _check_chain_matches() -> void:
var matches := _find_matches()
if matches.size() > 0:
_remove_matches(matches)
_apply_gravity_and_fill()
else:
is_animating = false
5.11 — Test
- Ctrl+S ile kaydedin
- F5 ile çalıştırın
Test senaryoları:
| Test | Beklenen Sonuç |
|---|---|
| 3’lü eşleşme yap | Şekerler silinir, üsttekiler düşer, üstten yenileri gelir |
| Düşme sonrası yeni eşleşme oluşsun | Zincir reaksiyon: yeni eşleşme de otomatik silinir |
| Eşleşme olmayan takas | Şekerler geri döner |
| Yeni gelen şekerler | Üstten animasyonla düşerek gelir |
Dikkat edilecek noktalar:
- Düşen şekerler hücrelere tam oturmalı (kayma olmamalı)
- Zincir reaksiyon sırasında tıklama engellenmeli
- Yeni gelen şekerler grid dışından (üstten) animasyonla inmeli
Konuyla ilgili Youtube videosu aşağıdadır…
Sonraki bölümde: Bonus şeker üretimi ekleyeceğiz — 4’lü eşleşme arrow, düz 5’li eşleşme rainbow, diğer 5’li eşleşmeler bomb verecek.