İçeriğe geç

3B Sahne Editörü

scene3d sahneyi veri yaptı: dünya, varlıklar, davranışlar ve kurallar bir JSON dosyasında duruyor. Editör de o dosyayı gözle kurmanın yolu — motorun içinde, oyunun kendi penceresinde çalışan bir düzenleyici.

Herhangi bir scene3d oyununda TAB’a bas: oyun donar, paneller açılır. Ayrı bir uygulama indirmen, ayrı bir derleme yapman gerekmez.

En kısa yol — herhangi bir scene3d oyununu çalıştır ve TAB’a bas. Editör her sahnede vardır; kurulum gerektirmez.

Kaydedebilmek için editöre bir hedef dosya söylemen gerekir:

import "scene3d";
str SAHNE = "examples/scenes/toplayici.scene.json";
func kur() {
editor_dosya3d(SAHNE); // F5 bu dosyaya yazacak
if (sahne_dosya3d(SAHNE) == 0) {
hata3d("SAHNE YUKLENEMEDI: " + SAHNE);
}
}
sahne_bilgi_dosya3d(SAHNE); // başlık/en/boy — pencereden ÖNCE
sahne3d(sahne_eni3d(), sahne_boyu3d(), sahne_basligi3d());
kurulumda3(kur);
oyna3d();

Bu, examples/scene3d_data_game.tpr dosyasının tamamına yakınıdır ve tek satır oynanış kodu içermez: düzenle → F5 → aynı JSON güncellenir → yeniden derleme yok.

Unity/Unreal düzeni: üstte araç çubuğu, solda hiyerarşi, sağda inspector, ortada görüntü penceresi.

DüğmeEtki
OYNATEditörden çık, oyunu başlat (editor3d(false))
SEC / TASI / OLCEK / DONDUREtkin kip (1 2 3 4 tuşları)
IZGARA nIzgarayı aç/kapa (G); kapalıyken IZGARA kapali yazar
GERI / ILERIGeri al / yinele (CTRL+Z / CTRL+Y)
KAYDETSahneyi hedef dosyaya yaz (F5)

Sağ uçta canlı sayaç: n varlik n davranis n kural.

Canlı varlıkların listesi. Bir satıra tıklamak onu seçer; seçili satır mavi vurgulanır. Fare panelin üstündeyken tekerlek listeyi kaydırır, altta ilk-son / toplam göstergesi durur.

Adı olan varlık adıyla, adsız olan etiket slot biçiminde listelenir; parantez içinde şekli yazar (kutu 4 (cube)).

Üstteki dört düğme yeni varlığı kameranın altına (yani baktığın yere) bırakır:

DüğmeÜrettiği
kutuprop / cube, 1×1×1
kureitem / sphere, 1×1×1
silinprop / cyl, 1×1×1
duvarwall / cube, 6×3×1

Seçili varlığın bütün alanları:

  • etiket ve şekil — döngüsel düğme. Etiket sırası player → item → wall → enemy → prop → player; şekil sırası cube → sphere → cyl → cube.
  • konum (x y z), boyut (sx sy sz — tam ölçü, yarım değil), yaw (derece) ve zemine otur düğmesi.
  • renk (r g b), yanında canlı örnek kutusu.
  • kati aç/kapa ve can (0 = can sistemi kapalı).
  • DAVRANISLAR — mevcut davranışların listesi (her satırda x ile silinir) ve altı ekleme düğmesi: +hareket +kovala +don +salin +devriye +ates.
  • Altta COGALT ve SIL.

Fare panellerin üstündeyken görüntü penceresi seçim ışınını atlar; yoksa her düğme basışı aynı anda arkadaki sahnede de bir seçim yapardı.

Oynat ve Dur — yazılmış hâl vs oynanmış hâl

Section titled “Oynat ve Dur — yazılmış hâl vs oynanmış hâl”

Bu, editörün en önemli davranışı. Model Unity/Unreal ile aynı: editör sahnenin sahibidir, “Oynat” bir kopyayı çalıştırır, “Dur” kopyayı atar. Oynarken olanlar hiçbir zaman kaydedilmez.

EylemNe olur
Oyun ilk açılırkurulum() (ve bölüm kurulumu) biter bitmez, ilk kareden önce sahnenin JSON’u “yazılmış hâl” olarak yakalanır
TAB ile editöre gir (= Dur)Simüle edilmiş durum atılır, yazılmış hâl geri yüklenir
TAB / OYNAT ile çık (= Oynat)Ekrandaki düzenlenmiş sahne artık yeni yazılmış hâldir; oyun onun kopyası üzerinde koşar
F5O anki sahne — yani yazılmış hâl — dosyaya yazılır

Geri yüklemede korunan şeyler (çünkü sahne biçiminde yer almıyorlar ve silinselerdi Dur’dan sonra oyun sessizce tepkisiz kalırdı):

  • elle yazılmış çarpışma/ölüm kancaları (carpisinca3, olunce3d)
  • bölüm tablosu ve o anki bölüm
  • skor
  • geri al / yinele yığınları — geri alma sahnede gezinmektir, yeni sahne açmak değil

Editör oyunu dondurur: fizik, davranışlar, kurallar, guncelle(), parçacıklar ve gündüz-gece saati işlemez. Editör bir oyun kipi değil — düzenlediğin şey ayağının altından kaymamalı.

TuşEtki
TABEditör ↔ oyun
Sağ tuş (basılı) + fareBak (yaw/pitch, ±88° sınırlı)
W A S DBakış yönünde uç (18 birim/sn)
Q / EAlçal / yüksel
Sol ShiftUçuş hızı ×3
Fare tekerleğiBakış ekseninde ileri/geri kay
Sol tuşSeç — TASI kipinde ayrıca sürükle
1 2 3 4SEC / TASI / OLCEK / DONDUR
GIzgara aç/kapa (0.5 ↔ kapalı)
DELSeçiliyi sil
CTRL+DÇoğalt (davranışlarıyla birlikte)
BOŞLUKZemine/araziye oturt
CTRL+Z / CTRL+YGeri al / yinele
F5Sahneyi dosyaya yaz
(TASI)y ± ızgara adımı
(OLCEK)sy ± ızgara adımı
(OLCEK)sx ve sz ± ızgara adımı
(DONDUR)yaw ∓ 15°

F1 (tanılama örtüsü) ve F2 (kaydı scene3d_log.txt’ye dök) editörde de çalışır — donmuş bir kareyi incelemek tam da gereken şeydir.

Sürükleme, varlığın kendi yüksekliğindeki yatay düzlemde olur: zemin düzlemini kullanmak, havadaki bir platformu tutunca onu yere düşürürdü.

Dosya düz JSON’dur. Anahtarlar İngilizce ve tek yazımlı: API’de TR+EN ikizler var ama bir veri biçiminde iki yazım kabul etmek belirsizlik üretir (hangisi kazanır?) ve ayrıştırıcıyı ikiye katlar. Kullanıcıya Türkçe görünen şey editörün etiketleri; dosyanın kendisi tek dilde.

Her alan isteğe bağlıdır; eksikse varsayılan kullanılır. Bu, biçimi ileri uyumlu yapar (eski dosya yeni motorda açılır) ve editörün yalnız gerekli alanları yazmasına izin verir.

AlanTürAnlamı
vintBiçim sürümü; şu an 1. Yazılır, yüklemede denetlenmez.
worldnesneDünya ayarları + kamera
entitiesdiziVarlıklar
rulesdiziKurallar
AlanTürVarsayılanAnlamı
titlestr"Tulpar 3B Sahne"Pencere başlığı
w / hint960 / 560Pencere boyutu
skynesne{"top": [r,g,b], "bottom": [r,g,b]} gradyan
fogfloat0Sis yoğunluğu (0 = kapalı)
groundfloatZemin düzleminin y’si. Alanın yokluğu anlamlı: zemin düzlemi yok demektir (yalnız arazi, ya da boşlukta platformlar).
gravityfloat26Yerçekimi
lightsbooltrueIşıklandırma
shadowsbooltrueGölgeler
daynightnesne{"len": 120, "time": 12, "frozen": false} — tam günün saniyesi, başlangıç saati (0..24), saat donuk mu
starsfloat-1-1 = otomatik (geceye bağlı); 0..1 elle sabitler
terrainnesneAşağıya bak
paintnesneArazi katman boyaması
waternesne{"y": 0, "color": [r,g,b], "alpha": 150, "physics": true}
slopenesne{"limit": 0, "slide": 18} — tırmanılamaz eğim ve kayma ivmesi
cameranesneAşağıya bak

terrain iki biçimden biri olur:

{"terrain": {"res": 129, "sx": 120, "sy": 14, "sz": 120, "noise": 4.5, "seed": 1}}
{"terrain": {"file": "harita.png", "sx": 120, "sy": 14, "sz": 120}}

paint alanları: low, mid, high, rock (renk) + mid_y (5), high_y (9), slope (42).

Renkler daima [r,g,b] ya da [r,g,b,a] dizisidir — bir renk seçici doğrudan buna denk düşer; paketli tamsayı (0xRRGGBBAA) insan için okunmaz.

Kamera ayrı ve varlıklardan sonra çözülür: hedefini adla gösterir.

AlanTürVarsayılanAnlamı
modestr"orbit""orbit" | "follow" | "fps"
targetstrHedef varlığın name’i
distfloat12Yatay mesafe (orbit/follow)
heightfloat8Yükseklik (orbit/follow)
eyefloat0Göz yüksekliği (yalnız fps)
fovfloat45Dikey görüş açısı
collidebooltrueKamera engel kaçınması
lockboolİmleç kilidi (yazılıysa uygulanır)
AlanTürVarsayılanAnlamı
namestrİsteğe bağlı ad. bul3d(ad) ve camera.target buna bakar.
tagstr"prop""player" | "item" | "wall" | "enemy" | "bullet" | "prop"
shapestr"cube""cube" | "sphere" | "cyl" | "ramp"
x y zfloat0Konum — merkez
sx sy szfloat1Tam boyut (yarım değil)
color[r,g,b]beyazRenk; [r,g,b,a] de olur
yawfloat0Y dönüşü (derece). Görsel değil: dönük kutu SAT ile dönük çarpışır.
solidbooletikete göreKatılık. Yalnız etiketin varsayılanından farklıysa dosyaya yazılır.
hpintCan sistemi (yoksa can yok)
behaviorsdiziDavranış listesi

Handle’lar yükleme sırasına bağlı olurdu, ad değil — bu yüzden kurallar ve kamera hedefi varlıklara adla atıfta bulunur.

Davranışlar oynanışı da veri yapar. Hepsi motorda zaten var olan işlevlerin üstüne biner (hareket3, chase3d, patrol3d, mermi3d) — yeni fizik yazılmadı, yalnız veriden sürülüyor. Böylece davranışla kurulmuş oyun ile elle yazılmış oyun aynı kodu koşturur.

typeAlanlar (varsayılan)Ne yapar
"move"speed (8), jump (0)Oyuncu girdisiyle hareket. jump: 0 → zıplama yok (tepeden bakışlı oyunlar).
"chase"target ("player"), speed (5), range (0)Etiketi verilen en yakın canlıyı kovalar. range: 0 = sınırsız; menzil dışında durur, kaymaz.
"patrol"x1 z1 x2 z2 (0), speed (4)İki nokta arasında gidip gelir
"spin"speed (90)Kendi ekseninde döner (derece/sn); süs
"bob"height (0.3), speed (1)Yerinde alçalıp yükselir (çevrim/sn); toplanabilir parıltısı
"shoot"interval (1), speed (20), life (2), target, range (0)Aralıkla ateş eder. target yoksa nişan yönüne, varsa en yakın hedefe dönüp atar.

target daima bir etiket adıdır, belirli bir varlık değil. Bir davranışın hedefini adla tutmak, hedef öldüğünde her karede ad araması gerektirirdi; etiket zaten “en yakın oyuncu” demenin doğal yolu.

Kod tarafındaki karşılıkları: hareket_davranis3d, kovala_davranis3d, devriye_davranis3d, don_davranis3d, salin_davranis3d, ates_davranis3d, davranislari_sil3d(id), davranis_sayisi3d().

“Ne olunca ne olur” da veri. Kurallar mevcut çarpışma taramasının üstüne biner; kurallar için ikinci bir çarpışma taraması yoktur.

İki tür var. on alanı türü seçer:

{"on": "hit", "a": "player", "b": "item", "do": "collect", "n": 50}
{"on": "cleared", "tag": "item", "do": "win"}
AlanTürVarsayılanGeçerli olduğu tür
onstr"hit""hit" | "cleared"
aetiket"player"hit — çarpışmada “ben”
betiket"item"hit — çarpışmada “öteki”
tagetiket"item"cleared — biten etiket
dostr"collect"ikisi de
nfloat0hit — skor / hasar miktarı

Eylemler (do):

doEtki
"collect"Ötekini öldür + kıvılcım + skora n ekle
"kill"Ötekini öldür (skorsuz)
"damage"Bana n hasar ver (oyuncu düşmana değdi)
"hurt"Ötekine n hasar ver (mermi oyuncuya değdi)
"win"Oyunu kazanılmış bitir
"lose"Oyunu kaybedilmiş bitir

Aynı etiket çifti için birden çok kural olabilir (hem skor hem hasar); hepsi işler.

Kod tarafındaki karşılıkları: carpisma_kurali3d(a, b, eylem, n), bitince_kurali3d(etiket, eylem), kural_sayisi3d(), kazandi3d(). Eylem sabitleri: ACT_COLLECT, ACT_KILL, ACT_DAMAGE, ACT_HURT, ACT_WIN, ACT_LOSE.

examples/scenes/toplayici.scene.json dosyasından kısaltılmış hâli:

{
"v": 1,
"world": {
"title": "Tulpar 3B — Veriyle Kurulmus Oyun", "w": 960, "h": 560,
"sky": {"top": [38, 52, 92], "bottom": [176, 198, 224]},
"fog": 0.014,
"ground": 0,
"gravity": 26,
"camera": {"mode": "orbit", "target": "kahraman", "dist": 16, "height": 11, "fov": 45}
},
"entities": [
{"name": "kahraman", "tag": "player", "shape": "cube",
"x": 0, "y": 2, "z": 12, "sx": 1.2, "sy": 2, "sz": 1.2,
"color": [110, 190, 240], "hp": 100,
"behaviors": [{"type": "move", "speed": 12, "jump": 13}]},
{"tag": "item", "shape": "sphere", "x": -14, "y": 1.4, "z": -12,
"sx": 1.2, "sy": 1.2, "sz": 1.2, "color": [255, 205, 60],
"behaviors": [{"type": "bob", "height": 0.35, "speed": 1.1}]},
{"tag": "enemy", "shape": "cube", "x": -20, "y": 1.5, "z": -20,
"sx": 1.4, "sy": 1.6, "sz": 1.4, "color": [222, 82, 74], "hp": 30,
"behaviors": [{"type": "chase", "target": "player", "speed": 4.5, "range": 40}]},
{"name": "kule", "tag": "prop", "shape": "cyl", "x": 0, "y": 1.5, "z": 0,
"sx": 2, "sy": 3, "sz": 2, "color": [150, 150, 160],
"behaviors": [{"type": "shoot", "interval": 1.6, "speed": 18, "life": 2.5,
"target": "player", "range": 26}]},
{"tag": "wall", "shape": "cube", "x": 0, "y": 2, "z": -30,
"sx": 62, "sy": 4, "sz": 2, "color": [126, 126, 134]}
],
"rules": [
{"on": "hit", "a": "player", "b": "item", "do": "collect", "n": 50},
{"on": "hit", "a": "player", "b": "enemy", "do": "damage", "n": 12},
{"on": "hit", "a": "bullet", "b": "player","do": "hurt", "n": 8},
{"on": "cleared", "tag": "item", "do": "win"},
{"on": "cleared", "tag": "player", "do": "lose"}
]
}

Bu JSON’da tek satır oynanış kodu yok ve oynanabilir bir oyundur. Adımlar:

  1. Barındırıcıyı yaz — bir kez. Yukarıdaki “Editörü açmak” bölümündeki ~15 satır. guncelle() yok; yalnız sahneyi yükler ve editöre hedef dosyayı söyler.
  2. Çalıştır ve TAB’a bas. Boş bir dünyada başlıyorsan hiyerarşideki kutu / kure / silin / duvar düğmeleri ilk varlıklarını verir.
  3. Oyuncuyu kur. Bir kutu ekle, inspector’dan etiketini player yap, canını 100 gir, +hareket davranışını tak. Kamera hedefi için sahne dosyasında ona bir name vermen gerekir (editörde ad alanı yoktur).
  4. Dünyayı kur. duvar düğmesiyle sınırları çiz, TASI kipinde sürükle, OLCEK’te oklarla boyutla, DONDUR’da 15°‘lik adımlarla çevir. Havada kalan bir şeyi BOŞLUK ile zemine oturt. Aynı düzendeki çok sayıda parça için CTRL+D.
  5. Oynanışı tak. Toplanacaklar: item + +salin. Düşmanlar: enemy + +kovala ya da +devriye. Kule: prop + +ates.
  6. Kuralları yaz. Kurallar için henüz panel yok — dosyaya elle ekle (yukarıdaki rules tablosu), ya da koddan bir kez carpisma_kurali3d(...) çağır. Editör onları korur ve geri yazar.
  7. F5. Dosya güncellendi. OYNAT’a bas, oyna, TAB ile geri dön, düzelt.

Buraya kadar bir sahne yalnız kod olarak vardı: kurulum() içinde elle yazılmış uret3(...) çağrıları. Bu, motoru kullanmanın tek yolunu “önce Tulpar öğren” yapıyordu ve konumları gözle ayarlamak imkânsızdı — sayıyı tahmin et, derle, bak, düzelt.

Sahnenin veri olması dört şeyi açıyor:

  1. Bir editör sahneyi okuyup yazabilir — kod üretmeden önce bile. Kod üretmek zorunda olsaydı, editörün yanında bir derleyici de bulunması gerekirdi.
  2. “Oynat” düğmesi tarayıcıda çalışıyor. Tarayıcı editörünün yanında derleyici yok, dolayısıyla kullanıcının yazdığı Tulpar’ı çalıştıramaz. Oynanışın da veri olması bu yüzden zorunluydu — davranışlar ve kurallar katmanının var olma sebebi bu. (Artık varsayım değil; aşağıdaki Tarayıcıda bölümüne bakın.)
  3. Bölümler dosya olur — oyunu yeniden derlemeden yeni bölüm eklenir.
  4. Aynı sahne hem web’de hem masaüstünde birebir aynı kurulur.

Biçim ayrıca insan tarafından düzenlenebilir kalacak şekilde yazılıyor: her varlık tek satır, yalnız etkin olan alanlar (kapalı su, kurulmamış arazi ya da varsayılan gökyüzü dosyaya girmez) ve kendi sayı yazıcısı — toString(120.0) "1.2e+02" verir; JSON bunu kabul eder ama insan okumaz. 3 ondalıkta yuvarlamak gidiş-dönüşü ayrıca durağan yapar: bir kez yuvarlandıktan sonra tekrar yuvarlamak değeri değiştirmez.

FonksiyonİngilizceDönerAmaç
sahne_yukle3d(kaynak)scene_load3dintJSON metninden yükle; yüklenen varlık sayısı (bozuk JSON’da 0)
sahne_dosya3d(yol)scene_file3dintDosyadan yükle
sahne_json3d()scene_json3dstrŞu anki sahneyi JSON olarak ver
sahne_kaydet3d(yol)scene_save3dboolDiske yaz
bul3d(ad)find3dintAdı verilen varlığın handle’ı; yoksa -1
sahne_bilgi3d(kaynak)scene_meta3dYalnız title/w/h oku (sahneye dokunmaz)
sahne_bilgi_dosya3d(yol)scene_meta_file3dAynısı, dosyadan
sahne_basligi3d()scene_title3dstrOkunan başlık
sahne_eni3d() / sahne_boyu3d()scene_w3d / scene_h3dintOkunan pencere boyutu

Yükleme tamdır: B sahnesini açmak A’nın gökyüzünü devralmaz. Dünya ayarları her yüklemede varsayılana çekilir — editör saniyede bir yeniden yüklüyor, bırakılan her ayar sıraya bağımlı bir hata olurdu.

Editörün bütün komutları dışarıdan çağrılabilir. Girdi işleme yalnız ince bir kabuk; asıl iş bu komutlarda — tarayıcı editörü de aynılarını çağırıyor.

FonksiyonAmaç
editor3d(ac) / editor_ac3d(ac)Editörü aç/kapa (Dur / Oynat)
editor_on3d()Editör açık mı
editor_dosya3d(yol) / editor_file3dKayıt hedefi
editor_kaydet3d() / editor_save3dŞimdi kaydet
ed_select3d(id) / ed_selected3d() / ed_deselect3d()Seçim
ed_pick_at3d(px, py)Ekran noktasından seç; handle ya da -1
ed_mode3d(m) / ed_mode_now3d()Kip — ED_SELECT ED_MOVE ED_SCALE ED_ROTATE
ed_grid3d(adim) / ed_grid_now3d()Izgara adımı (0 = kapalı)
ed_move3d(id, x, y, z)Mutlak taşı (ızgaraya oturtur)
ed_nudge3d(id, dx, dy, dz)Göreli ittir
ed_resize3d(id, sx, sy, sz)Boyutla (alt sınır = ızgara adımı)
ed_rotate3d(id, yaw)Döndür
ed_add3d(etiket, sekil, x, y, z)Yeni varlık, seçili gelir
ed_duplicate3d(id)Çoğalt — davranışlarıyla birlikte
ed_delete3d(id)Sil (davranışlarını da temizler)
ed_drop3d(id)Zemine/araziye oturt
ed_mark3d()Geri alma noktası koy
ed_undo3d() / ed_redo3d()Geri al / yinele (bool döner)
ed_undo_count3d() / ed_redo_count3d()Yığın uzunlukları

Seçim, kamera-engel taramasının kullandığı ışın-kutu testini çağırır — ayrı bir test yazmak aynı geometriyi iki yerde tutmak olurdu. Dönük kutuların doğru seçilmesi bu yüzden bedava geldi.

  • "shape": "model" yazılır ama okunmaz. Serileştirici SHAPE_MODEL varlıkları "model" olarak yazar; yükleyici bu adı tanımaz ve cube olarak yükler. Model varlıklarını koddan kur.
  • Şekil döngüsü yalnız üç şekli gezer (cubespherecyl). ramp ve model için dosyayı elle düzenle.
  • Inspector’da ad alanı yok. Editörde eklenen varlıklar adsızdır, yani bul3d ve camera.target onları göremez. Ad vermek için dosyaya "name" ekle — kaydetme bunu korur.
  • Davranış satırında yalnız ilk iki sayı düzenlenebilir. patrol’ün dört noktası ve shoot’un life/range alanları için dosyaya git.
  • Kurallar için panel yok. Kurallar rules dizisinde ya da koddan kurulur; editör onları korur ve geri yazar.
  • Mermiler ve ömürlü varlıklar kaydedilmez — onlar çalışma zamanı ürünü, sahne değil.
  • v denetlenmiyor. Alan yazılıyor ama yükleyici sürüm denetimi yapmıyor.
  • Boyutlar tam ölçü, yarım değil; konum merkezdir. scene3d’nin geri kalanıyla aynı sözleşme.

Sahne biçimi, davranışlar, kurallar ve editör tests/scene3d_engine.test.tpr içinde 66 test ile korunuyor (motorun toplam süiti 212 test). Hepsi pencere açmadan koşuyor.

Işın testlerinin beklentisi kodun formülünden değil fov’un tanımından geliyor: ekranın dikey kenarından geçen ışın ileri yönle tam fov/2 açı yapar. Aynı formülü tekrarlayan bir test aynı yanlışı onaylardı.

Terminal window
./build.sh suites # bütün tests/*.test.tpr süitleri
./tulpar tests/scene3d_engine.test.tpr

Editör WebAssembly’ye derleniyor ve tarayıcıda çalışıyor — aynı kaynak, ayrı bir uygulama yok:

Terminal window
mkdir -p assets && cp <bir>.ttf assets/ui.ttf # okunabilir arayüz fontu
TULPAR_WEB_ASSETS=assets tulpar build --target=web \
examples/scene3d_editor.tpr -o editor
# HTTP üzerinden servis et; file:// çalışmaz

Masaüstünden üç şey farklı ve üçü de bırakılmış bir eksiklik değil, verilmiş bir cevap:

Font pakete konuyor. Tarayıcıda sistem font dizini yok, dolayısıyla TULPAR_WEB_ASSETS olmadan editör raylib’in 10 piksellik bitmap fontuna düşüyor ve okuması yorucu oluyor. Aday listesinin başında assets/ui.ttf, fonts/ui.ttf ve ui.ttf var — sistem yollarından önce bakılıyor, yani fontu pakete koymak yetiyor. Bulunamazsa konsol bunu ve çaresini söylüyor.

“Kaydet” dosyaya değil TARAYICI DEPOSUNA yazıyor. Emscripten’in sanal dosya sistemi sayfa ömürlüdür: yazdığın şey yenileyince yok olur, üstelik çağrı yine “başarılı” der. Bu sessiz veri kaybıdır; o yüzden scene_save3d web’de localStorage kullanıyor (anahtar sahne yolundan türüyor, yani aynı sayfadaki iki sahne birbirini ezmiyor) ve yenilemeyi atlatıyor.

“İNDİR” işin sayfadan çıkış yolu. Tarayıcı deposu sayfanın içinde kalıyor. Menü şeridinde yalnız web’de görünen bir İNDİR düğmesi var — sahneyi .json olarak veriyor. Masaüstünde böyle bir ayrım yok: “Kaydet” zaten dosyaya yazıyor, ikinci düğme yalnız kafa karıştırırdı.

Pakete konmuş sahne dosyası BAŞLANGIÇ hâlidir; kayıtlı düzenlemelerin onu ezer — yoksa her yenileme yaptığın işi geri alırdı.