Godot'da Prosedürel Taş Sur, Kapı ve Köprü Üretmek
10 dk okuma

Kalecik'te kale kurmanın temel hareketi bir çizgi. Parmağınızı ekranda sürüklersiniz, arkasından taş taş örülen bir sur gelir. Keskin bir köşe yaparsanız oraya bir kule oturur. Arnavut kaldırımı bir yolu surun içinden geçirirseniz kemerli bir kapı açılır; göletin üstünden geçirirseniz yol köprüye dönüşür. Bir alanı karalarsanız o alan bir evin tabanı olur. Kalecik, iPhone ve iPad için hazırladığım, sakin bir kale ve köy kurma oyunu.
Bu yazıda bir parmak çizgisinin sura, kapıya, köprüye ve eve nasıl dönüştüğünü adım adım anlatıyorum. Baştan dürüst olayım: oyunun kodunu ben yazmadım. Kodu paralel çalışan Claude Code oturumları, yani yapay zekâ ajanları yazdı. Ben ne yapılacağına karar verdim, her build'i iPhone'da denedim ve gördüğümü geri bildirdim. Projenin bütününü boş klasörden iPhone'a giden yolu anlattığım yazıda bulabilirsiniz. Buradaki kod parçaları oyunun gerçek kodundan (Godot 4, GDScript) alındı; kısaltıp açıklama ekledim.
Sıra şöyle: çizgi önce temiz bir eğriye, eğri metreyle ölçülen bir yola, yol da taşlara dönüşüyor. Ardından kuleler, silgi, kapı, köprü ve ev kuralları geliyor.
Çizgiden yola: yumuşatma ve WallPath
Titrek bir çizgiyi eğriye çevirmek
Dokunmatik ekrandan gelen noktalar düzensizdir: hızlı sürüklerseniz seyrek, yavaş sürüklerseniz sık gelir, el de biraz titrer. Bu noktalardan doğrudan örülen bir sur, her titremeyi taşa kazırdı. Ajanın kurduğu çözüm üç satır (main.gd, _shape_stroke):
pts = PathTools.resample(pts, 0.9) # 0,9 m aralıklı, eşit noktalar
pts = PathTools.chaikin(pts, 3, closed) # üç tur köşe kesme
pts = PathTools.resample(pts, 0.25) # sonraki hesaplar için sık ve düzgünİlk satır çizgiyi 0,9 metrede bir nokta olacak şekilde yeniden örnekler. Böylece sürükleme hızınız ne olursa olsun noktalar eşit aralıklı olur ve küçük titremeler aradaki boşlukta kaybolur. İkinci satır Chaikin köşe kesme algoritmasını üç kez uygular. Üçüncü satır sonucu 25 santimetre aralıklı bir diziye çevirir; sonraki bütün hesaplar bu dizinin üstünde çalışır.
Chaikin'in fikri çok basit: her kenarın dörtte bir ve dörtte üç noktasını alır, köşeyi atarsınız. Her turda köşeler biraz daha yuvarlanır, üç turdan sonra el çizimi bir yay gibi görünür:
## Chaikin köşe kesme. Açık eğrilerde uç noktalar yerinde kalır.
static func chaikin(src: PackedVector2Array, iterations: int, closed: bool) -> PackedVector2Array:
var p := src
for it in iterations:
var n := p.size()
if n < 3:
return p
var q := PackedVector2Array()
if not closed:
q.append(p[0])
var count := n if closed else n - 1
for i in count:
var a := p[i]
var b := p[(i + 1) % n]
q.append(a.lerp(b, 0.25)) # kenarın dörtte biri
q.append(a.lerp(b, 0.75)) # ve dörtte üçü
if not closed:
q.append(p[n - 1])
p = q
return pYumuşatmadan önce iki karar daha veriliyor. Çizginin ucu var olan bir kuleye ya da sura yakınsa uç oraya yapıştırılıyor; iki sur böylece gerçekten birleşiyor. Çizgi 8 metreden uzunsa ve başıyla sonu arasında 2,5 metreden az mesafe kaldıysa kapalı bir halka sayılıyor. Oyuncunun parmağını tam başladığı noktada kaldırması gerekmiyor.
Metreyle ölçülen yol: WallPath
Yumuşatılmış noktalar hâlâ yalnızca bir dizi. Taş dizmek içinse şu soruya hızlı cevap lazım: "Yolun başından s metre sonra neredeyim, hangi yöne bakıyorum?" wall_path.gd bunu yay uzunluğu parametrelemesiyle çözüyor. Her noktaya kadar birikmiş uzunluğu (cum) bir kez hesaplıyor, sonra istenen s için ikili aramayla doğru segmenti bulup iki nokta arasında ara değer alıyor:
func pos(s: float) -> Vector2:
if closed and length > 0.0:
s = fposmod(s, length) # halkada başa sar
else:
s = clampf(s, 0.0, length)
var lo := 0
var hi := pts.size() - 1
while hi - lo > 1: # ikili arama: s hangi segmentte?
var mid := (lo + hi) >> 1
if cum[mid] <= s:
lo = mid
else:
hi = mid
var seg := cum[hi] - cum[lo]
if seg <= 0.00001:
return pts[lo]
return pts[lo].lerp(pts[hi], (s - cum[lo]) / seg)Normal, yani surun dışa baktığı yön, tek bir segmentten değil, s'nin 30 santimetre gerisi ile 30 santimetre ilerisi arasındaki farktan hesaplanıyor. Bu merkezi fark, köşe noktalarında normalin aniden sıçramasını engelliyor; taşlar köşeden yumuşakça döner.
Bundan sonra her şey metre cinsinden konuşuyor: kapı "surun 12. metresinde", silginin bıraktığı parça "3 ile 7,5 metre arası".
Taş taş sur: her sıraya, her taşa kendi tohumu
Kalecik'te hazır bir 3B model dosyası yok; her taş kodla üretilen küçük bir ızgara. masonry.gd suru sıra sıra örüyor. Her sıranın yüksekliği 30 ile 42 santimetre arasında rastgele seçiliyor (en alt sıra daha kalın), sıra kendi içinde 50 santimetre ile 1 metre arası taşlara bölünüyor. Kapı gibi açıklıklar sıraya "delik" olarak veriliyor ve o aralığa taş konmuyor.

Taş taş örülmüş surlar; her taşın biçimi kendi tohumundan geliyor.
Asıl püf noktası rastgeleliğin nasıl tohumlandığı. Sur siz sürüklerken uzuyor ve her güncellemede yeniden hesaplanıyor. Tek bir rastgele sayı üreticisi kullanılsaydı sur her uzadığında bütün taşlar yer değiştirir, duvar gözünüzün önünde kaynardı. Kodda her sıranın ve her taşın kendi tohumu var:
while y < y_top - 0.08:
var h := course_rng.randf_range(0.30, 0.42) # sıra yüksekliği
rng.seed = sd * 31 + row * 7919 + 17 # bu sıranın taş boyları
var spans := _row_spans(a, b, row_holes, 0.7 if row == 0 else 0.5, 1.25 if row == 0 else 1.0, rng)
for k in range(0, spans.size(), 2):
var sm := (spans[k] + spans[k + 1]) * 0.5
if not _in_range(path, sm, s_from, s_to):
continue # bu taşı başka bir parça kuruyor
# taş başına tohum: hangi parça kurarsa kursun taş aynı görünür
stone_rng.seed = sd * 131 + row * 104729 + k
_stone(buf, path, side, off, spans[k] + GAP * 0.5, spans[k + 1] - GAP * 0.5, y + GAP * 0.5, y + h - GAP * 0.5, stone_rng)
y += h
row += 1Sıra, yolun başından sonuna doğru bölündüğü için sur uzadığında yalnızca en sondaki taşların boyu değişiyor; öndeki taşlar aynı tohumla aynı sırada aynı boyda çıkıyor. Bir sonraki bölümdeki hızlandırma tam olarak bu özelliğe dayanıyor.
Tek bir taş ise "yastık" biçimli bir ızgara: kenar halkası harç seviyesine gömülü, iç kısmı 8,5 santimetre dışarı çıkıyor, yüzeye çok hafif bir eğim veriliyor ve renk birkaç taş tonundan birinden küçük bir kaydırmayla seçiliyor. Taşların arasında 3,5 santimetrelik harç payı kalıyor. Gölgeli bir sahnede bu kadarı, elle örülmüş görüntüsüne yetiyor.
Çizerken yalnızca kuyruğu kurmak
İlk sürümde her sürüklemede bütün sur baştan kuruluyordu. Kısa bir surda sorun yoktu; ama 50 metrelik bir surda tek bir güncelleme M1 Mac'te 80 ms sürüyordu, akıcı bir çizim için fazlasıyla yavaş. Çözüm, suru 4 metrelik parçalara bölmek oldu (castle_view.gd):
const CHUNK := 4.0
const TAIL := 4.0 # çizerken yalnızca bu son bölüm hâlâ şekil değiştirebilir
func preview_update(path: WallPath, sd: int, opts: Dictionary) -> void:
var n := maxi(1, ceili(path.length / CHUNK))
var jobs := []
for k in range(_preview_stable, n): # sabitlenmiş parçaları atla
jobs.append(_chunk_job(path, sd, k * CHUNK, minf(path.length, (k + 1) * CHUNK), "", opts))
var built := _run_jobs(jobs)
# ... kurulan mesh'ler sahnedeki eski parçaların yerine geçer
_preview_stable = clampi(floori((path.length - TAIL) / CHUNK), 0, n)Parmak ilerledikçe son 4 metrenin gerisinde kalan parçalar "sabit" sayılıyor ve bir daha kurulmuyor; her güncellemede yalnızca kuyruk yeniden örülüyor. Bu, taş tohumları sayesinde güvenli: sabitlenmiş bir parçadaki taşlar, bütün sur baştan kurulsaydı nasıl olacaksa aynen öyle. Canlı güncelleme aynı makinede ortalama 8 ms'ye, en kötü durumda 11 ms'ye indi. Bu değerlerin hepsi M1 Mac'te ölçüldü; telefonda ölçülmüş bir rakamım yok.
Bitmiş surlarda da aynı mantık çalışıyor. Her parçanın mesh'i bir anahtarla önbelleğe alınıyor: taban yol, parça numarası, aralık ve surun imzası (tür, yükseklik, kapılar, merdivenler gibi taşları etkileyen her şey). Bir düzenleme yalnızca imzası değişen parçaları yeniden kuruyor. Kullanılmayan 150 parça mesh'i de geri alma ve yineleme için bekletiliyor; geri aldığınızda sur yeniden örülmüyor, önbellekten geliyor. Büyük yeniden kurulumların iş parçacıklarına nasıl dağıtıldığını ve telefonun ısınma sorununu Godot mobil performans yazısında anlatıyorum.
Kuleler ve silgi

Silgi suru böler; açık uçlara taş ayaklar oturur.
Kuleler nereye oturur?
Bir sur çizip bıraktığınızda kuleler kendiliğinden geliyor: açık bir surun iki ucuna, iki taş surun kesiştiği yere ve keskin köşelere. Köşeyi bulmak için ajan şu yolu izledi: yol boyunca yarım metrede bir durup 2 metre gerisi ile 2 metre ilerisi arasındaki yön değişimine bakıyor. 60 dereceden keskin dönüşler aday oluyor, adaylar keskinliğe göre sıralanıyor ve birbirine 5,5 metreden yakın olanlardan yalnızca en keskini kalıyor:
## Düze yakın bölümlere kule konmaz; gerçek köşelere konur.
func _corner_spots(path: WallPath) -> Array:
var cand := []
var s := 0.0 if path.closed else 3.0 # açık surun uçlarına ayrıca kule konuyor
var s_end := path.length if path.closed else path.length - 3.0
while s < s_end:
var a := path.pos(s - 2.0)
var b := path.pos(s)
var c := path.pos(s + 2.0)
var ang := absf((b - a).angle_to(c - b))
if ang > deg_to_rad(60.0):
cand.append([s, ang])
s += 0.5
cand.sort_custom(func(x, y): return x[1] > y[1]) # en keskin köşe önce
var out := []
for cd in cand:
var ok := true
for o in out:
var d := absf(float(o) - float(cd[0]))
if path.closed:
d = minf(d, path.length - d)
if d < 5.5: # seçilmiş bir kuleye çok yakın
ok = false
break
if ok:
out.append(cd[0])
return outKural bilerek temkinli. İlk sürümü telefonda denediğimde hızlıca karalanmış bir çizgi kule yığınına dönüyordu; geri bildirimim kısaca "saçma oto kuleler" oldu. Açı eşiği, mesafe kuralı ve ayarlardaki "Oto kule" anahtarı bundan sonra geldi. Kulenin boyu, yarıçapı ve çatısı da tohumdan seçiliyor; sonradan dokunup değiştirilebiliyor. Dört çatı tipi var: külah, ahşap galerili, mazgallı taç ve piramit çatılı kare kule.
Silgi: taşları kıpırdatmadan bölmek
Silgi, prosedürel bir yapının en çok zorlandığı yer. Bir surun ortasını sildiğinizde kalan iki parçanın yeniden hesaplanmasını, taşların kaymasını istemezsiniz; kale yıkılmış gibi değil, bilerek açılmış bir geçit gibi görünmeli. castle.gd bunu veri modeliyle çözüyor. Köy düz veri olarak tutuluyor, mesh'ler her zaman bu veriden kuruluyor. Bir sur, bir taban yolun [s0, s1] aralığındaki parçası:
var bases := {} # id -> {pts, closed, seed, kind, height, gates: Array}
var walls: Array = [] # {id, base, s0, s1}Silgi taban yola hiç dokunmuyor. Parçanın üstünde 20 santimetrede bir örnek alıyor, fırçanın altında kalan örnekleri işaretliyor ve açıkta kalan ardışık örnek gruplarını yeni parçalar olarak saklıyor; 60 santimetreden kısa kırıntıları atıyor. Kapalı bir halkada başlangıç noktasının iki yanında kalan parçalar tek parça olarak birleşiyor. Taşlar taban yoldan ve tohumdan türetildiği için kalan her taş olduğu yerde kalıyor. Kapılar da taban yolun üzerinde metre cinsinden saklandığı için bölünmeden etkilenmiyor.
Yan etkisi de güzel: geri alma ve kaydetme, birkaç dizinin kopyasından ibaret; mesh önbelleği gerisini hallediyor.
Yol sura girerse kapı, suya girerse köprü
Kalecik'te en sevdiğim kural bu: yolun nereden geçtiğine göre yapı kendini değiştiriyor. İki çizginin kesiştiği yerleri bulmak için PathTools.crossings her segment çiftine klasik doğru parçası kesişim testini uyguluyor ve kesişmeyi surun başından itibaren metre olarak döndürüyor. Sur çizildiğinde:

Göletin üstünden çizilen yol, merdivenli bir taş köprüye dönüştü.
# yolun ya da evin üstünden geçen taş sura kemer ("geçit") açılır
if kind in Castle.STONE_KINDS:
for bid in _bases_of_kind("path"):
for s in PathTools.crossings(castle.bases[bid].pts, shaped.pts, shaped.closed):
castle.add_gate(w.base, s)Yol çizildiğinde aynı test ters yönde yapılıyor. add_gate bazı kapıları reddediyor: açık bir surun ucuna 1,6 metreden yakın kapı açılmıyor, iki kapı arasında en az 3 metre kalıyor. Taş örücü kapının 2 metrelik aralığını, kapı yüksekliğine kadar her sıraya delik olarak veriyor ve üstüne kemeri kuruyor.
Köprüde soru kesişme değil, ıslaklık. _split_at_water yolu 25 santimetrede bir örnekliyor ve arazinin su seviyesinin altında kaldığı örnekleri ıslak sayıyor. Islak bölgeler iki kıyıya da 1,5 metre taşacak şekilde büyütülüyor; böylece köprü suyun kenarında değil, karada başlıyor. Yolun kuru kısımları taş döşeli yol, ıslak kısımları köprü oluyor. Güverte ortada hafifçe kamburlaşıyor (bridge.gd):
## Güverte ortada hafifçe yükselir; kemerlere yer açılır ve köprü gibi okunur.
static func hump(L: float) -> float:
return clampf(L * 0.09, 0.3, 1.5)
static func deck_at(base: float, s: float, L: float) -> float:
return base + hump(L) * sin(clampf(s / L, 0.0, 1.0) * PI)Kambur, köprü boyunun yüzde 9'u kadar; ama 30 santimetreden az, 1,5 metreden fazla değil. Ayaklar yaklaşık 3,6 metrede bir diziliyor, aralarına kemer kuruluyor, iki yana taş ya da ahşap korkuluk geliyor, güverte yerden yüksekte bitiyorsa merdiven iniyor.
Merdivende öğretici bir hata yaşandı. Merdivenin yönü ilk başta yolun normalinden hesaplanıyordu. Ama normalin hangi tarafa baktığı, güvertenin hangi yöne uzandığını söylemez; sonuçta her merdiven güvertenin altına dönüyordu. Düzeltme, yönü yolun kendisinden, uç nokta ile 30 santimetre içerideki nokta arasındaki farktan almak oldu. Kemer taşlarının "düşen bloklar gibi" durması da benzer bir geometri hatasıydı; taşlar elipsin teğet ve normaline hizalanarak düzeltildi.
Karalamadan ev: en küçük sınırlayıcı dikdörtgen
Ev aracında çizdiğiniz şeklin kendisi değil, onu en sıkı saran dikdörtgen kullanılıyor. PathTools.min_rect önce noktaların dışbükey zarfını alıyor. Sonra zarfın her kenarını sırayla bir eksen kabul edip bütün noktaları o eksene ve ona dik eksene izdüşürüyor; alanı en küçük çıkan kutu evin tabanı oluyor:

Çizilen alana oturan ev; dokununca tutamaçlar ve düzenleme menüsü çıkıyor.
static func min_rect(src: PackedVector2Array) -> Dictionary:
var h := hull(src) # dışbükey zarf
var best := {}
var best_area := INF
for i in h.size():
var e := h[(i + 1) % h.size()] - h[i]
if e.length_squared() < 1e-8:
continue
var u := e.normalized() # bu kenar bir eksen
var v := Vector2(-u.y, u.x) # ve ona dik olan
var lo := Vector2(INF, INF)
var hi := Vector2(-INF, -INF)
for p in h:
var q := Vector2(p.dot(u), p.dot(v)) # iki eksene izdüşüm
lo = Vector2(minf(lo.x, q.x), minf(lo.y, q.y))
hi = Vector2(maxf(hi.x, q.x), maxf(hi.y, q.y))
var size := hi - lo
if size.x * size.y < best_area:
best_area = size.x * size.y
var mid := (lo + hi) * 0.5
best = {center = u * mid.x + v * mid.y, angle = u.angle(), w = size.x, d = size.y}
return bestBu yöntemin işe yaramasının nedeni, en küçük alanlı dikdörtgenin bir kenarının her zaman dışbükey zarfın bir kenarıyla çakışması. Bütün açıları denemek yerine yalnızca zarfın kenarlarını denemek yetiyor.
Ölçüler 25 santimetreye yuvarlanıp izin verilen aralığa sıkıştırılıyor. Yalnızca dokunursanız varsayılan bir kulübe geliyor. Kapı her zaman kameraya bakan tarafa konuyor. Var olan bir evin ucuna aynı derinlikte çizilen ev onunla birleşip uzuyor; yanına dik çizilen ev L ya da T kanadı olarak yapışıyor. Taş surun üstüne çizilen ev ise sura kemerli bir geçit açıp kapı evine dönüşüyor.
Bu kuralları kim koydu, nasıl sınandı?
İlk dosyalar (wall_path.gd, path_tools.gd, masonry.gd) ilk mesajdan on dört dakika sonra oluştu. Yarım saat sonra Mac'te ilk demo vardı: çizgi çiz, taş sur ve otomatik kule gelsin. Evler ve surda kemerli kapı açan yol aynı gece eklendi; köprüler ve birleşmeleri sonraki turlarda geldi. Benim payım neyin olması gerektiğini söylemek ve sonucu telefonda denemekti. Saçma yerlere dikilen oto kuleler, köprü uçları, köprülerin birbirine bağlanması gibi geri bildirimler ve "prosedürel modelleme kısmında feci eksiklikler var" cümlesi benden çıktı; çözümler ajanlardan.
Ajanlar her kuralı oyunun içine yerleşik senaryolu bir teste bağladı. Test sahte dokunuşlarla sur, yol ve ev çiziyor, sonra "yol: duvardan geçince kapı açıldı", "göletten geçen yol köprü oldu", "silgi avluyu kesti: döngü açıldı" gibi kontrolleri tek tek doğruluyor. Bir değişiklik bir kuralı bozarsa test bunu, oyun telefona kurulmadan önce yakalıyor.
Kalecik iPhone ve iPad için geliştiriliyor; sürüm 1.0 App Store incelemesine hazırlanıyor, Android sürümü de Google Play için yolda. Tek seferlik ücretli, reklamsız ve internetsiz bir oyun. Bu surları kendi parmağınızla çizmek isterseniz mağaza bağlantıları yayına girdiğinde Kalecik sayfasında olacak. Oyunun sesleri de hiçbir kayıt kullanılmadan kodla üretildi; onu oyun müziğini kodla üretmeyi anlattığım yazıda okuyabilirsiniz.
Sık Sorulan Sorular
Chaikin algoritması nedir, neden Bézier eğrisi kullanılmadı?
Chaikin, her kenarın dörtte bir ve dörtte üç noktalarını alıp köşeleri kesen basit bir yumuşatma yöntemidir. Bézier için kontrol noktası seçmeniz gerekir; Chaikin ise eldeki noktalarla doğrudan çalışır ve birkaç satırdır. El çizimini temizlemek için bu yeterli.
Kalecik'te hazır 3B model kullanılıyor mu?
Hayır. Taşlar, kuleler, köprüler ve evler dahil bütün geometri kodla üretiliyor. Hazır ya da yapay zekâyla üretilmiş bir 3B model dosyası yok.
Sur uzarken taşlar neden yer değiştirmiyor?
Çünkü her taşın rastgeleliği kendi tohumundan geliyor: surun tohumu, sıra numarası ve taşın sıradaki yeri. Sur uzadığında yalnızca sondaki taşların boyu değişiyor, öndeki taşlar aynı tohumla aynı çıkıyor. Bu yüzden çizerken yalnızca son 4 metreyi yeniden kurmak yetiyor.
Kalecik ne zaman ve hangi platformlarda çıkacak?
Kalecik iPhone ve iPad için; App Store sürümü çok yakında, Google Play sürümü de hazırlanıyor. Tek seferlik satın alınıyor, reklam, abonelik ve uygulama içi satın alma yok, internetsiz çalışıyor. Güncel durum Kalecik sayfasında.
İlgili Yazılar
Telifsiz Oyun Müziği: Bütün Sesleri Kodla Üretmek
Kalecik'te tek bir ses kaydı yok: taş, kuş, koyun ve 37 müzik parçası Python ile üretildi. Döngüsüz çalma, müzik bekçisi ve LUFS ile ses kontrolü.
Yapay Zekâ Ajanlarıyla Mobil Oyun Yapmak: Kalecik
Boş bir klasörden iPhone’a: Kalecik’i beş paralel Claude Code oturumunu yöneterek nasıl yaptım, hangi kararları verdim, nerede duvara tosladık.
iPhone'u Isıtan Oyun: Godot'da Mobil Performans
Godot oyunum iPhone 15 Pro Max'i ısıttı. Kök neden 120 fps çıktı; kare hızı yöneticisi, gölge vekilleri, çimen parçaları ve iş parçacıklarıyla çözüm.