Tarayıcıda açık bir dünya oluşturmak, bölüm 29: Tek kontrolcü, her beden
Oleg Sidorkin, Cinevva CTO'su ve Kurucu Ortağı
Burada yeni misiniz? Seri rehberini kullanın. Bu rehber, spike'ın ne olduğunu açıklıyor ve tüm bölümlere bağlantı veriyor.
28. bölüm çim ve örtülmeyi ele aldı. Yirmi sekiz bölüm boyunca içinde durabileceğiniz bir dünya oluşturduk: okuyabildiğiniz arazi, yüzebildiğiniz su, ufka kadar kalitesini koruyan bitki örtüsü ve değiştirdiklerinizi hatırlayan bir sunucu. Bu bölüm, tüm bunların içinde hareket eden şeyle ilgili ve aynı zamanda tüm motorun karşılığını verdiği nokta; çünkü hedef bir oyuncu kontrolcüsü değil. Hangi bedeni yönettiğini, animasyonlarının nereden geldiğini veya direksiyonda bir insanın mı yoksa yapay zekânın mı bulunduğunu önemsemeyen bir kontrolcü. Spike 58, hareketi, lokomasyon hakkında hiçbir şey bilmeyen bir fizik motorunun üzerinde takılabilir davranışlardan oluşan bir yığın olarak kuruyor, ardından üç farklı bedeni aynı motor kopyasıyla çalıştırarak mimariyi kanıtlıyor. Spike 59, tek bir satırını bile değiştirmeden bu kontrolcüye gerçek ve yeniden hedeflenmiş bir avatar bağlıyor. Spike 60 ise hiç yeniden hedefleme gerektirmeyen farklı bir animasyon paketi bağlıyor ve klip seçme mantığını gerçekten test edebileceğiniz bağımsız bir yapıya çıkarıyor.
Yürüme hakkında hiçbir şey bilmeyen bir fizik motoru
Spike 58'i yeni bir sekmede aç ↗ · Kaynak kodunu görüntüle
Tasarım kuralı katı ve meselenin özü de bu: kapsül motoru hiçbir lokomasyon davranışına sahip değil. Yürüme yok, koşma yok, zıplama yok, sürtünme yok, azami hız sınırı yok, hatta yerçekimi bile yok. Kinematik bir kapsülü araziye göre entegre ediyor ve bundan fazlasını yapmıyor. Yürüme, kayma, süzülme, tırmanma, yüzme, çömelme ve dayanıklılık gibi tüm lokomasyon davranışları, motora kaydedilen bağımsız bir kontrolcüde yaşıyor. Motor her karede tüm kontrolcüleri güncelliyor, her birine kontrolü isteyip istemediğini soruyor ve hızı en yüksek öncelikli talip olan kontrolcünün yazmasına izin veriyor. Yürümenin özel bir konumu yok. Yalnızca her zaman evet diyen en düşük öncelikli kontrolcü; dolayısıyla başka hiçbir şey devreye girmediğinde varsayılan davranış oluyor. Bir kontrolcü altı küçük işlevden oluşuyor: etkin olmasa bile her karede kendi iç durumunu güncelleyen bir tick, kare üzerinde hak iddia eden saf bir wantsControl koşulu, yalnızca kazananın çalıştırdığı, hızı yazan ve isterse kendi yerçekimini uygulayan bir applyForces ile isteğe bağlı onEnter, onExit ve stateName. Yüzme, kaldırma kuvveti yayı motorun ayağı zemine oturtma işlemiyle çakışmasın diye arazi işleme kontrolünü devralmak üzere applyForces içinden ownsCollision: true döndürüyor.
Lokomasyon olmayan şeyler bile bu sözleşmeye sızıyor ve bu da bir sonraki fikri zorunlu kıldı. Mimariyi değerli kılan parça kanallar. Daha önceki tek kanallı tasarım her şeyi aynı hakemlikten geçiriyordu; bu da bir dayanıklılık izleyicisinin veya çömelme duruşunun, yalnızca kendi hesaplamalarını yapabilmek için lokomasyonmuş gibi davranıp ardından false döndüren bir wantsControl hilesiyle kontrolü reddetmesi gerektiği anlamına geliyordu. Çözüm, kontrolcüleri birbirinden bağımsız olarak hakemlik yapan ve sabit bir sırayla uygulanan adlandırılmış kanallara ayırıyor: önce kaynak, ardından duruş, sonra lokomasyon. Kaynak önce çalışıyor, çünkü dayanıklılığın azalması gibi yazdığı değerleri diğerleri okuyor. Duruş ikinci sırada çalışıyor, çünkü çömelmenin kapsül yüksekliğini küçültmesi, yürüme kontrolcüsü azami hızı sınırlamak için bu değeri okumadan önce gerçekleşmek zorunda. Lokomasyon en son çalışıyor ve kare başına hız yazımının sahibi oluyor. Böylece bir dayanıklılık gözlemcisi, bir çömelme değiştiricisi ve etkin bir yüzme kontrolcüsü, her biri kendi kanalında olacak şekilde sorunsuzca birlikte var oluyor; hiçbir kontrolcünün ne olduğu konusunda yalan söylemesi gerekmiyor. Demo, tüm bunları etkin kontrolcüye göre renk değiştiren çıplak bir kapsülle görselleştiriyor; böylece hakemliğin gerçekleşmesini izleyebiliyorsunuz: yeşil yürüme, dik bir yamaçta kayma kontrolü devraldığında turuncuya; gölde yüzme kazandığında camgöbeğine; havadayken süzülmeye dokunduğunuzda krem rengine dönüyor.
Tek motor, üç beden
“Hiçbir lokomasyona sahip değil” iddiasının asıl sınavı oyuncu değil. Asıl mesele, aynı motorun hiç değiştirilmeden oyuncu olmayan bir şeyi de yönetip yönetemediği. createCapsuleEngine, modül düzeyinde durumu, singleton'ı ve örneğe özgü yan etkileri olmayan saf bir fabrika olduğundan spike bunu üç kez örnekliyor. Oyuncu, eksiksiz kontrolcü kümesine sahip bir örnek. Binilebilir at, yalnızca bir yürüme kontrolcüsü kaydeden ikinci bir örnek; bu da tüm fikri kelimenin tam anlamıyla hayata geçiriyor: bir bedenin hareket repertuvarı yalnızca kaydettiğiniz kontrolcülerden oluşuyor. Dolayısıyla at düz zeminde daha hızlı ve fiziksel olarak tırmanamıyor, yüzemiyor veya süzülemiyor; çünkü bu kontrolcüler hiç eklenmedi. Otonom NPC ise oyuncuyla birlikte her karede güncellenen üçüncü bir örnek. Kendi girdisini üreten bir gezinme kontrolcüsü tarafından yönetiliyor; böylece beden, klavyede kimsenin eli olmadan kendi kendine yön buluyor. Motor, bedenlerinden birinin at veya diğerinin yapay zekâ tarafından yönetildiğini asla öğrenmiyor. Hepsi, yalnızca farklı kontrolcü listelerine sahip aynı kapsül entegratörü.
Binme, kasıtlı olarak motorun dışında yaşayan tek parça. Kontrolü oyuncu ile at arasında değiştirmek iki motorun koordinasyonunu gerektiriyor; bir kontrolcü ise tek bir motorun içinde çalışıyor ve bu sınırın ötesini göremiyor. Bu nedenle binme mantığı ana sistem düzeyinde bulunuyor: bu karede hangi motorun ilerletileceğine karar veren, diğerini donduran, biniciyi saniyenin dörtte üçü boyunca smoothstep ile yumuşakça eyere taşıyan ve kameraya hangi bedeni izleyeceğini söyleyen küçük bir durum makinesi. Motor fabrikası bunların hiçbirinden haberdar olmuyor. Mimari tam olarak bu sınırı çiziyor ve koruyor. Tek bir bedene ait davranışlar kontrolcüdür; bedenler arasındaki koordinasyon ise ana sistemin görevidir. Bunları birbirinden ayrı tutmak, dördüncü veya kırkıncı bir bedenin hiçbir yeni maliyet getirmemesini sağlıyor.
Tarayıcı olmadan test edildi
Motor hiçbir window, document veya Three.js nesnesine erişmediği için tüm sistem başsız çalışıyor. Spike; arazi arayüzünün sahtesini oluşturan, motoru betiklenmiş girdilerle kare kare çalıştıran ve ortaya çıkan durum üzerinde doğrulamalar yapan bir Node test düzeneğiyle geliyor. Böylece oynayarak test ederken yakalanması son derece zahmetli olan regresyonlar bunun yerine bir betikte yakalanıyor: zıplama ve yere inme geçişi, yüzmeye giriş ve çıkış eşikleri, yüzey düzleştiğinde tırmanmanın otomatik olarak temizlenmesi, dayanıklılık tükenme oranları. Aynı girdi ve zaman adımı dizisini iki kez besleyip çıktı durumlarının bayt düzeyinde aynı olduğunu kontrol etmek, motorun deterministik olduğunu sabitliyor; gelecekte ağ bağlantılı bir yapının dayanacağı özellik de bu. Test düzeneğinin yakaladığı bir regresyonu özellikle saklamakta fayda var: yürünebilir zeminden yürünemeyecek kadar dik bir yamaca geçmek eskiden kayma kontrolcüsünü etkinleştiriyor ve oyuncuyu yokuş yukarı geri sektiriyordu. Çözüm, bir alçalma kapısı oldu. Kayma yalnızca kapsül gerçekten yamacın üzerine düşüyorsa başlıyor; dolayısıyla dik bir yüzeye yatay olarak yürümek artık kaymayı tetiklemek yerine hareketi düzgünce engelliyor. “Yürünemeyen bir yamaca doğru yürümek oyuncuyu engeller” doğrulamasını yapan test de bu düzeltmenin kalıcı olmasını sağlıyor.
Kontrolcülere dokunmadan gerçek bir avatar bağlamak
Spike 59'u yeni bir sekmede aç ↗ · Kaynak kodunu görüntüle
Spike 59, kontrolcü katmanının bedenden gerçekten ayrıştırılıp ayrıştırılmadığını sınayan test. Renkli kapsülü gerçek bir iskeletli karakterle değiştiriyor: Quaternius Universal Animation Library kliplerinin üzerine yeniden hedeflendiği 3MIKE FBX iskeleti. Kontrolcüler ise hiç değişmiyor. Bağlantı noktası tek bir dize. Her kontrolcü zaten stateName üzerinden bir durum adı bildiriyor: idle, walk, run, jump, fall, land, slide, glide, swim, swimIdle. Avatar katmanı bu adı bir takma ad tablosu üzerinden yeniden hedeflenmiş klibe eşliyor ve geçiş sırasında çapraz soldurma uyguluyor. Yürüme kontrolcüsü kendi alt durumunu yerde olma durumu, dikey hız ve yatay hıza göre dinamik olarak seçiyor. Böylece tek bir yürüme kontrolcüsü bekleme, yürüme, koşma, zıplama, düşme ve yere inme durumlarını yönetiyor; avatar da yalnızca bildirilen adı izliyor. Bu spike tek oyunculu olduğundan avatar, önceki ağ bağlantılı spike'lardaki kablo tamsayısı dolaylamasını ve çok karakterli soyutlamayı kaldırıyor ve durum adını doğrudan bir klibe eşliyor. Bunun kanıtı, kapsülden iskeletli insana yapılan tüm görsel yükseltmenin lokomasyon kodunda sıfır satırı değiştirmiş olması; takılabilir bir kontrolcünün sağlaması gereken fayda da tam olarak bu.
Yeniden hedefleme gerektirmeyen bir paket ve test edilebilir bir seçici
Spike 60'ı yeni bir sekmede aç ↗ · Kaynak kodunu görüntüle
Spike 60 üçüncü bir beden olan Synty'nin POLYGON Base Locomotion paketini bağlıyor ve yükleyici neredeyse hiçbir şey yapmıyor. Her klip, aynı Synty iskeletinin gömülü bir kopyasını ve tek bir işlenmiş animasyonu taşıyan bağımsız bir FBX olarak geliyor. Karakter iskeletiyle her klip aynı kemik adlarını kullandığından klibi fbx.animations[0] üzerinden alıp arada herhangi bir yeniden hedefleme kütüphanesi olmadan doğrudan karakterin mixer'ında oynatabiliyorsunuz. Three.js, animasyon izi hedeflerini nesne kimliği yerine kemik adına göre çözümlüyor; dolayısıyla eşleşen iskelet için hazırlanmış Synty veya Mixamo tarzı bir paket doğrudan çalışıyor. Bu, önceki spike'taki UAL yoluyla bilinçli olarak oluşturulan karşıtlık. Kaynak klipler ve hedef iskelet farklı iskelet sistemleri için hazırlandığından UAL yolu kapsamlı bir yeniden hedefleme gerektiriyor. Aynı kontrolcü, aynı durum adı bağlantısı, arkasında ise tamamen farklı iki animasyon hattı.
Spike 60'ın diğer yarısı, klip seçme mantığını test edilebilir hâle getiriyor. Hangi klibin oynatılacağını seçmek yoruma açık çok sayıda eşik içeriyor ve bu mantık, test edilemediği FBXLoader ve DOM'un yanında avatar katmanının içine gömülmüştü. Spike, seçiciyi ne Three.js'e ne de window'a dokunan saf işlevlere çıkarıyor: bu işlevler yalın bir oyuncu kaydı (hız, yatay hız, bakış yönü, yerde olma durumu, zemin normali, çarpma hızı) ve klip eylemi yerine geçen bir nesne alıp bir klip takma adı dizesi döndürüyor. Bu da eşiklerin adlandırılmış ve doğrulanan sabitler olarak yaşamasını sağlıyor. Zıplama, yürüme kontrolcüsünün gerçek sprint eşiğiyle uyumlu hız aralıklarına göre yürüme, koşma ve sprint varyantlarına ayrılıyor; inişler çarpma hızına göre yumuşak, orta ve sert olarak ayrılıyor; yokuş yukarı ve yokuş aşağı klip varyantları, yaklaşık yedi derecenin altında düz klibin kullanıldığı bir ölü bölgeye sahip eğim izdüşümü nokta çarpımından etkinleşiyor; beklemeden harekete geçiş köprüsü her zaman ileri yönü seçiyor, böylece hareketsiz başlangıçta hiçbir zaman geriye doğru bir klip oynatılmıyor; durma köprüsü ise minimum döngüde kalma süresiyle sınırlandırılıyor, çünkü Synty'nin ayak fazına göre hazırlanmış durma klipleri yaklaşık bir saniyelik tasarlanmış yavaşlama içeriyor ve oyuncunun zar zor attığı bir adıma eklendiğinde gülünç görünüyor. Küçük bir durum debounce mekanizması, birkaç kare boyunca düşme durumunu geciktiriyor; böylece bir birleşim noktasında zemin temasının tek kareliğine kaybolması animasyonun titremesine neden olmuyor. Tüm bunları görüntüleme kabuğundan çıkarmak, kuralların GPU olmadan birim testine tabi tutulabilmesini sağlıyor. Bu, motorun Spike 58'de kazandığı başsız çalışma disiplininin aynısı ve tahmin ederek ayarladığınız hareket hissiyle kesin biçimde sabitleyebildiğiniz hareket hissi arasındaki farkı oluşturuyor.
Bu bölümde değinilen teknoloji
Lokomasyondan bağımsız bir fizik motoru. Kapsül motoru, kinematik bir bedeni araziye göre entegre ediyor ve yürüme, koşma, zıplama, sürtünme, hız sınırı veya yerçekimi davranışlarından hiçbirine sahip değil. Her davranış; tick, wantsControl, applyForces ve isteğe bağlı onEnter/onExit/stateName sunan kayıtlı bir kontrolcü. Yürüme yalnızca her zaman evet diyen, en düşük öncelikli varsayılan davranış ve bir kontrolcü arazi işleme kontrolünü devralmak için ownsCollision: true döndürebiliyor (yüzme bunu yapıyor, böylece kaldırma kuvveti yayı ayağı zemine oturtma işlemiyle çakışmıyor).
Bağımsız hakemlik kanalları. Kontrolcüler, birbirinden ayrı olarak hakemlik yapan ve sabit bir sırayla uygulanan adlandırılmış kanallara (kaynak, duruş, lokomasyon) kaydoluyor. Böylece dayanıklılık gibi gözlemciler ve çömelme gibi değiştiriciler, kontrolü alıyormuş gibi yapıp ardından reddetmek yerine etkin lokomasyonla birlikte var olabiliyor. Kaynak yazımları (dayanıklılık), duruş ve lokomasyon tarafından okunuyor; duruş yazımları (çömelme kapsülünün yüksekliği), lokomasyonun hız sınırı tarafından okunuyor; lokomasyon ise en son çalışıyor ve hız yazımının sahibi oluyor. Tek fabrika, birçok gövde. createCapsuleEngine, singleton içermeyen saf bir fabrikadır; böylece aynı motor oyuncuyu, yalnızca yürüyebilen binilebilir bir atı ve yapay girdilerle kendi kendini yöneten bir NPC'yi çalıştırır. Bir gövdenin hareket repertuvarı, tam olarak kayıtlı kontrolcü kümesinden oluşur; dolayısıyla at tırmanamaz veya yüzemez, çünkü bu kontrolcüler hiç eklenmemiştir. Binme mekaniği bir kontrolcüde değil, ana uygulama katmanında bulunur; çünkü birbirinin iç durumunu göremeyen iki motoru koordine eder. Bkz. GPU güdümlü LOD.
Başsız, deterministik testler. Motor window, document veya Three.js'e dokunmaz; böylece bir Node test düzeneği onu kare kare çalıştırarak zıplama ve yere iniş, yüzme eşikleri, tırmanmanın otomatik olarak sonlandırılması ve dayanıklılık tüketimi hakkında doğrulamalar yapar. Aynı girdiyi iki kez yeniden oynatıp çıktının aynı olduğunu kontrol etmek determinizmi kanıtlar. Ayrıca bir regresyon testi, dik bir yüzeye doğru yatay yürüyüşün oyuncuyu geriye kaydırmasını önleyen alçalma geçidini sabitler.
Test edilebilir bir seçiciye sahip, gövdeden bağımsız avatar bağlama. Kontrolcüler bir durum adı dizesi bildirir ve görsel katman bunu bir kliple eşler; böylece kapsülü yeniden hedeflenmiş bir 3MIKE + UAL avatarıyla değiştirmek, hareket kodunda hiçbir değişiklik gerektirmez. Synty POLYGON klipleri rig ile aynı kemik adlarını paylaşır ve UAL yolundaki ağır yeniden hedefleme işleminden farklı olarak yeniden hedefleme olmadan oynatılır (Three.js, izleri kemik adına göre bağlar). Klip seçici; zıplama hızı aralıkları, iniş şiddeti, eğim izdüşümü varyantları, her zaman ileri yönlü bir boşta bekleme geçişi, durma geçişi için minimum süre koruması ve düşme durumu debounce mekanizmasına yönelik adlandırılmış sabitler içeren saf fonksiyonlara ayrılmıştır. Böylece hareket hissi tahmin edilmek yerine birim testleriyle doğrulanabilir.
30 bölümün 29. bölümü. Önceki: Bölüm 28 - Ufka uzanan çimler ve kendini gizleyen zemin Sonraki: Bölüm 30 - Duvarlara saygı duyan bir kamera Seri rehberi: /tr/blog/2026-02-25-open-world-browser-series-guide