Godot'da Pogo Fiziği: Kendiliğinden Zıplayan Karakter
5 dk okuma

BOINGWARD'da karakter hiç durmadan zıplayan bir pogo sopasının üstünde. Zıplama tuşu yok; pogo her inişte kendiliğinden yeniden fırlıyor, oyuncu yalnızca havadayken yön veriyor. Böyle bir oyunda fizik "iyi hissettirmek" zorunda değil, öngörülebilir olmak zorunda: aynı yaklaşım ve aynı girdi, her seferinde neredeyse aynı sonucu vermeli. Yoksa oyuncu düştüğünde neden düştüğünü anlayamaz.
Bu yazı o fiziğin Godot 4.7 ve Jolt üzerinde nasıl kurulduğunu anlatıyor: durum makinesi, temas kuralları, fırlatma formülü, ilk ayarın neden bozuk çıktığı ve fiziğin kare hızından bağımsız olduğunun nasıl test edildiği. Oyunun bir günde nasıl yapıldığını serinin merkez yazısında anlattım.
RigidBody, ama her yazma tek yerden
Pogo bir RigidBody3D. Fizik motoru çarpışmaları bulsun istiyoruz ama hareketi motorun sürtünmesine ve sekmesine bırakmak istemiyoruz. Bu yüzden gövde açılışta sadeleştiriliyor:
func _ready() -> void:
gravity_scale = 0.0 # yerçekimini kendimiz uyguluyoruz
lock_rotation = true # gövde devrilmez; eğilme yalnızca görsel
contact_monitor = true
max_contacts_reported = 8
continuous_cd = true # hızlı düşüşte zeminden geçmesin
var material := PhysicsMaterial.new()
material.friction = 0.0
material.bounce = 0.0 # sekmeyi motor değil biz hesaplıyoruz
physics_material_override = materialKuralın kendisi projenin kural dosyasında yazılı: fiziğe yapılan her yazma _integrate_forces içinde olur. Bu, Unity'deki FixedUpdate'in Godot karşılığı; sabit fizik adımında, motorun durumu üzerinden çalışıyor. Girdi, kamera ve arayüz _process içinde kalıyor. Pogo global_position ile asla taşınmıyor; tek istisna yeniden doğma, o da bir sonraki fizik adımında uygulanıyor.
Durum makinesi
Pogonun beş durumu var: RESPAWN, AIRBORNE, COMPRESSION, STUNNED ve FINISHED. Her fizik adımı aynı iskeletten geçiyor:
func _integrate_forces(s: PhysicsDirectBodyState3D) -> void:
_collect_contacts(s)
var v := s.linear_velocity
match state:
State.COMPRESSION:
v = _compress_surface_velocity # sıkışırken platformla birlikte hareket et
if _time >= _compression_end:
v = _launch()
State.AIRBORNE, State.STUNNED:
v = _apply_zones(v, dt) # yerçekimi, rüzgâr, düşük yerçekimi
v.y = maxf(v.y, -tuning.terminal_speed)
if state == State.AIRBORNE:
v = _steer(v, desired, dt)
v = _evaluate_contacts(v)
s.linear_velocity = vSıkışma 0,03 saniye sürüyor; ekranda görünmeyecek kadar kısa ama pogo bir hareketli platforma indiğinde o anı platformla birlikte geçirsin diye var.
Her temas zıplama değildir
Bir temasın zıplamaya dönüşmesi için dört şart aranıyor:
- Temas pogonun ucunda olmalı: gövdenin en altındaki 0,4 metrelik bölge. Daha yukarıdaki temaslar gövde çarpmasıdır.
- Yüzey zıplanabilir olmalı:
no_bouncegrubundaki yüzeyler sayılmıyor. - Pogo yüzeyden uzaklaşıyor olmamalı ve son fırlatmanın üstünden 0,12 saniye geçmiş olmalı. Böylece aynı temas iki kez sayılmıyor.
- Yüzey yeterince düz olmalı: normalin dikey bileşeni en az 0,35. Daha dik bir yüzeye uç değerse pogo sekiyor.
Temasın kaybolduğu tek bir fizik adımı yüzünden zıplama kaçmasın diye uç teması iki adım daha "taze" sayılıyor. Gövdeyle saniyede 4 metreden hızlı bir çarpma ise pogoyu 0,45 saniyeliğine sersemletiyor ve komboyu sıfırlıyor. Uzun bir kontrol kaybı yok; ceza kısa ve anlaşılır.
Fırlatma formülü
Asıl karar fırlatma anında veriliyor ve bu hesap, sahneye hiç dokunmayan saf bir fonksiyonda: PogoLaunchMath.compute. Saf olduğu için birim testi yazmak kolay.
Yön, üç vektörün ağırlıklı toplamı:
var dir := normal * t.normal_weight \ # 1.0: yüzeyin normali
+ pogo_up * t.pogo_weight \ # 1.4: pogonun eğildiği yön
+ tangential / t.momentum_reference_speed * t.momentum_weight # 0.6: yandan gelen hız
dir = dir.normalized()Yüzey normalinden en fazla 75 derece sapmasına izin veriliyor. Hız ise şöyle:
var speed := t.base_bounce_speed \ # 12 m/s
+ impact * t.impact_transfer \ # çarpma hızının küçük bir kısmı
+ minf(combo, t.max_combo) * t.combo_bonus # en fazla 5 x 0.2
speed = clampf(speed, t.min_launch_speed, t.max_launch_speed) # 8..22 m/sSonra yandan gelen hızın yüzde 60'ı korunuyor, toplam hız 26 m/s ile, yatay hız 10 m/s ile sınırlanıyor ve platform hareket ediyorsa onun hızı ekleniyor. Bütün sayılar koddan değil, PogoTuning adlı bir kaynak dosyasından geliyor; kodda "sihirli sayı" yasak.
İlk ayar neden bozuktu
Oyunun ilk tasarımında çarpma hızının yüzde 35'i bir sonraki fırlatmaya aktarılıyordu. Kâğıt üzerinde mantıklı: yüksekten düşen daha yükseğe zıplasın. Ölçünce durum farklı çıktı. Hiç dokunmadan zıplayan bir pogo, kararlı durumda saniyede yaklaşık 21 metreyle fırlıyor ve 9 metreye çıkıyordu. Bir platform oyunu için fazlasıyla yüksek.
Sebep basit bir geri besleme. İnişteki hız kabaca bir önceki fırlatma hızına eşit; her fırlatma bir öncekinin bir kısmını taşıyorsa hız kendi kendini büyütüyor. Kararlı durum şöyle hesaplanıyor:
v = (temel + kombo bonusu) / (1 - aktarım)Aktarım 0,35 iken bölen 0,65; küçük bir temel hız bile büyüyor. Aktarım 0,05'e indirildi: (12 + 5 x 0,2) / 0,95 ≈ 13,7 m/s, yani yaklaşık 3,9 metrelik bir tepe. Bu değer parkur tasarımının temel ölçüsü oldu (en düşük zıplama 3 metre, kararlı zıplama 4 metre). Aynı mantıkla yatay hız da sınırlandı; yoksa her atlayış bir öncekinden biraz daha uzağa gidiyor ve atlama mesafesi kartopu gibi büyüyordu.
Dokunmatik ekran için hoşgörü
Telefonda başparmakla nişan almak fareyle nişan almaktan zor. Bu yüzden görünmez yardımlar var, ama sınırları kesin:
- İniş yardımı: Pogo yüzey normaline 12 dereceden yakınsa fırlatma yönü normale doğru yüzde 60 bükülüyor. Daha eğik inişlere dokunulmuyor.
- Kusursuz iniş: 6 dereceden dik bir iniş "kusursuz" sayılıyor, ama yalnızca en az 3 metrelik gerçek bir atlayıştan sonra. Aynı yerde zıplayıp durmak kusursuz iniş üretmiyor.
- Havadaki kontrol: Yavaşken tam, hızlanınca azalıyor; saniyede 14 metrede yüzde 35'e iniyor.
Tasarım belgesindeki kural açık: yardımlar niyeti tercüme eder, kaçırılmış bir platformu asla kurtarmaz. İniş yardımı yalnızca geçerli bir temasta devreye giriyor; havadaki pogoyu platforma çekmiyor.
Hareketli platformlar ve Jolt
Hareketli bir platforma inen pogo, platformla birlikte gitmeli. Burada bir sürprizle karşılaşıldı: Jolt, kinematik gövdeler için temas noktasındaki hızı sıfır bildiriyor. Çözüm, hareketli her parçanın kendi yüzey hızını bir yöntemle vermesi:
# Jolt kinematik gövdeler için sıfır bildiriyor; hareketli yüzeyler hızını kendisi veriyor.
if collider != null and collider.has_method(&"surface_velocity_at"):
sv = collider.surface_velocity_at(pos)Fırlatmada pogo platforma göre hesaplıyor ve platformun yatay hızını tamamen tutuyor; kayan bir platformdan zıplarken ayağının altından kaymıyor.
Kare hızından bağımsızlık testi
Fizik saniyede 60 adımda çalışıyor ama ekran 30, 60 ya da 120 karede çizilebiliyor. Fiziğin sonucu çizim hızına bağlıysa bir telefonda geçilen platform başka bir telefonda geçilemez. Bunu önlemek için girdi _process içinde okunup saklanıyor, fizik adımında tüketiliyor ve bütün hesap _integrate_forces içindeki adım süresiyle yapılıyor.
Kuralın tutup tutmadığını bir test ölçüyor. Test betiği aynı senaryoyu üç ayrı çizim hızında çalıştırıp tek satırlık bir imza yazıyor: zıplama sayısı, ulaşılan tepe yüksekliği ve sabit bir numara denemesinin sonucu.
for fps in 30 60 144; do
godot --headless --path . --fixed-fps "$fps" -s tests/run_tests.gd -- sig
done
# SIG bounces=… apex=… trick=… üç satır da birebir aynı olmalıÜç satırdan biri farklıysa test paketi kırmızıya dönüyor. Ekranda akıcılık için Godot'nun fizik enterpolasyonu açık; yeniden doğma ve ışınlanma anında enterpolasyon sıfırlanıyor ki karakter iki nokta arasında kayarak görünmesin. 120 karelik mod da bu sayede eklendi: fizik değişmeden 60 adımda kalıyor, yalnızca çizim sıklaşıyor.
Sık Sorulan Sorular
Godot'da karakter fiziği için CharacterBody mi RigidBody mi?
Klasik bir platform oyununda CharacterBody3D çoğu zaman daha kolay. BOINGWARD'da hız neredeyse tamamen fırlatma anında belirlendiği ve hareketli yüzeylerle temas önemli olduğu için RigidBody3D seçildi; sürtünme ve sekme kapatılıp hız _integrate_forces içinde elle yazılıyor.
Zıplama yüksekliği neden kendi kendine artıyordu?
Her fırlatma, çarpma hızının bir kısmını bir sonrakine taşıyınca hız geri beslemeyle büyüyor. Kararlı hız temel hızın (1 - aktarım) oranına bölümü. Aktarım 0,35'ten 0,05'e inince tepe 9 metreden 4 metre civarına düştü.
Oyun fiziğinin farklı FPS'lerde aynı çalıştığını nasıl test ederim?
Fizik hesabını sabit adıma taşıyın, girdiyi çizim karesinde okuyup fizik adımında tüketin ve aynı senaryoyu --fixed-fps ile farklı çizim hızlarında çalıştırıp küçük bir sonuç imzasını karşılaştırın. İmzalar birebir aynı olmalı.
Jolt'ta hareketli platform karakteri neden taşımıyor?
Jolt kinematik gövdeler için temas noktası hızını sıfır bildiriyor. Hareketli parçanın kendi hızını bir yöntemle (burada surface_velocity_at) vermesi ve karakterin bu hızı fırlatma hesabına katması gerekiyor.
İlgili Yazılar
Risk ve Ödül: Pogo Oyununda Hava Numaraları
Tek cümlelik bir istekten sayılarla tanımlı bir sisteme: BOINGWARD'da hava numaraları nasıl isteğe bağlı, riskli ve kare hızından bağımsız bir ödüle dönüştü.
Bir Günde 3B Oyun: Yapay Zekâ Ajanlarıyla BOINGWARD
Unity paketiyle başlayıp aynı sabah Godot'ya geçtim. Paralel Claude Code oturumlarıyla bir günde 148 commit ve telefonda oynanan bir 3B pogo oyunu çıktı.
Ölçülmüş Metriklerle Seviye Tasarımı ve Test Botu
BOINGWARD'da parkurlar ölçülmüş zıplama değerlerinden üretiliyor, bir bot her atlamayı gerçek fizikle deniyor. Madalya süreleri de bu botun koşusundan çıkıyor.