- Ini adalah tutorial yang memperluas arsitektur ECS dan pemrograman metalinguistik di atas lingkungan pengembangan game Common Lisp ke contoh dungeon crawler nyata
- Setelah membaca peta XML Tiled dengan cl-tiled, objek CLOS tidak digunakan langsung, melainkan dipindahkan ke komponen ECS untuk memisahkan rendering, collision, dan manajemen memori
- Dengan mengombinasikan prefab tile, pointer gambar, indeks parent-child, dan finalizer, tutorial ini menghindari pemuatan duplikat dan double free, serta memanfaatkan properti kustom Tiled layaknya data
- Pemain dan musuh melakukan pergerakan, transisi animasi, dan penanganan collision melalui sistem ECS; musuh melacak target sambil menghindari dinding dengan pencarian jalur A* berbasis cl-astar
- Dengan menambahkan UI berbasis Nuklear, objek naratif, pause, hingga kondisi kemenangan, tutorial ini menyelesaikan contoh dungeon crawler kecil berukuran sekitar 500 baris
Memulai proyek dan eksekusi dasar
- Membuat dungeon crawler kecil dengan UI menggunakan arsitektur Entity-Component-System dan teknik pemrograman metalinguistik yang dibahas di Part 1
- Binary demo yang dapat dijalankan dan kode sumber tersedia di repositori GitHub ecs-tutorial-2
- Lingkungan pengembangan mengasumsikan lingkungan pengembangan game Common Lisp dari Part 1, dan distribusi Quicklisp diperbarui dari REPL SBCL
(ql-util:without-prompting (ql:update-all-dists))
- Membuat proyek baru
ecs-tutorial-2dengan templat cookiecutter-lisp-game, dan pada contoh ini memilihliballegrosebagai backend - Setelah menautkan direktori proyek ke
local-projectsQuicklisp, ukuran jendela disrc/main.lispdiubah menjadi 1280×800 - Jika menjalankan
(ql:quickload :ecs-tutorial-2)dan(ecs-tutorial-2:main), akan tampil jendela hitam dengan resolusi yang ditentukan serta penghitung FPS
Peta Tiled dan penyimpanan ECS
- Untuk membuat peta dungeon digunakan editor peta open-source Tiled
- Tiled adalah alat lintas platform dan lintas engine, serta menyimpan data peta sebagai XML
- Di Common Lisp, cl-tiled memuat file Tiled sebagai objek Lisp
- Tileset contoh menggunakan Dungeon Tileset II - Extended
- Karena tile asli berukuran 16×16 terlalu kecil, tile diperbesar 200% dengan ImageMagick dan digunakan sebagai tile 32×32
level1.tmxdan file tileset dapat diperoleh dariResources.zipyang disediakan dalam tutorial
- Menambahkan dependensi
cl-tiledkeecs-tutorial-2.asd, lalu membuatsrc/map.lispbaru untuk memisahkan kode pemuatan dan tampilan peta - Di
src/package.lisp,cl-tileddidaftarkan dengan nickname lokaltiled
Alasan memindahkan objek CLOS ke komponen ECS
cl-tiledmengembalikan data peta sebagai objek CLOS, sehingga mudah dieksplorasi di REPL- Jika objek ini digunakan langsung dalam game loop, biaya dispatch saat runtime dapat membesar
- Untuk mengisi jendela 1280×800 dengan tile 32×32, diperlukan minimal 40×25 = 1000 tile
- Dalam demo terpisah, pada Ryzen 5 3600 12-core, FPS turun dari 20.000 menjadi 600 saat rendering peta diaktifkan
- Tambahannya sekitar
1/600 - 1/20000 = 0.0016detik per frame, yakni lebih dari 1,5 ms
- Memindahkan data yang dibaca
cl-tiledke storagecl-fast-ecsdapat mengurangi dispatch dan meningkatkan pemanfaatan cache CPU - Menambahkan dependensi
cl-fast-ecs, lalu memanggilecs:make-storagediinitdanecs:run-systemsdiupdate
Komponen peta, tile, dan prefab
mapadalah komponen tag yang menandai entitas peta yang sudah dimuatmap-tilemerepresentasikan tile individual, dan memiliki slot Booleanobstacleuntuk menandai apakah tile tersebut merupakan rintangan seperti dinding atau pintu tertutup- Komponen
parentmenunjukkan entitas peta mana yang menjadi induk dari tile dan objek terkait peta- Dengan menetapkan
:index childrenpada slotentity, entitas anak dari parent tertentu dapat ditemukan dengan cepat - Indeks berbasis hash table open-addressing sehingga menyediakan pencarian rata-rata O(1), tetapi ada biaya pembaruan saat pembuatan dan penghapusan
- Dengan menetapkan
- Menambahkan hook ke
ecs:*entity-deleting-hook*agar saat entitas parent dihapus, entitas anak yang ditemukan melalui indekschildrenjuga ikut dihapus - Komponen
imagehanya menyimpan pointer C keALLEGRO_BITMAP- Gambar tileset dipotong menjadi bagian 32×32 dengan
al_create_sub_bitmap, lalu pointernya disimpan
- Gambar tileset dipotong menjadi bagian 32×32 dengan
map-tile-prefabadalah prefab tile yang memiliki ID globalgidmilik tile Tiled- Pada
gid,:index map-tile-prefab :unique tditetapkan agar satu entitas prefab dapat ditemukan berdasarkan ID - Tile nyata di atas peta menyalin
imagedan komponen lain dari prefab, tetapi memiliki posisi lewat komponenpositionterpisah
- Pada
- Finalizer
imagehanya memanggilal_destroy_bitmapketika entitas adalahmap-tile-prefab- Tujuannya menghindari double free karena beberapa tile peta berbagi pointer
ALLEGRO_BITMAPyang sama
- Tujuannya menghindari double free karena beberapa tile peta berbagi pointer
positiondansizemenyimpan koordinat serta ukuran layar sebagaisingle-float- Karena
liballegromenangani koordinat layar sebagai floating point presisi tunggal demi kompatibilitas OpenGL, pendekatan yang sama diikuti
- Karena
Rendering gambar dan pemuatan peta
- Sistem
render-imagesmerender entitas yang memilikipositiondanimage- Mengaktifkan dan menonaktifkan sprite batching dengan
al_hold_bitmap_drawing - Menggambar gambar pada koordinat tertentu dengan
al_draw_bitmap - Karena prefab tidak memiliki
position, prefab tidak diproses oleh sistem ini
- Mengaktifkan dan menonaktifkan sprite batching dengan
load-bitmapadalah fungsi pemuatan gambar yang membungkusal_load_bitmapdenganal:ensure-loadedtile->specmembuat spesifikasi objek ECS untuk pembuatan prefab tile- Entitas peta parent
- Potongan gambar tile
- ID tile global Tiled
- Ukuran tile
load-tile-prefabmemeriksa lewat indeksmap-tile-prefabapakah prefab sudah dimuat; jika belum, prefab dibuat denganmake-objectload-tilemenyalin komponen dari prefab dan menambahkanpositionsaat membuat entitas tile peta nyataload-mapmenelusuri tileset dan layer dari objek CLOS yang dibaca dengantiled:load-map- Memuat gambar tileset dan membuat setiap tile sebagai prefab
- Membuat setiap sel pada layer tile sebagai entitas dan menyalin data prefab
- Urutan layer Tiled dipertahankan sesuai urutan di editor, dan
make-entitymenjamin nomor entitas yang meningkat- Karena sistem memproses entitas dari yang lebih lama, tile pada layer atas digambar belakangan sehingga menutupi layer di bawahnya
- Menyimpan semua tile sebagai entitas terpisah bukan satu-satunya jawaban; peta statis juga dapat dirender terlebih dahulu ke buffer
Animasi Tile
- Tiled mendukung tile animasi, sehingga elemen seperti obor atau air mancur ajaib dapat direpresentasikan
- Menambahkan
common.lispdananimation.lispuntuk memisahkan komponen umum serta komponen dan sistem terkait animasi - Komponen
animation-framemerepresentasikan satu frame dari animasisequenceadalah nama animasi dan disimpan sebagai tipekeyword- Frame untuk animasi tertentu ditemukan melalui indeks
sequence-frames durationadalah durasi frame dalam satuan detik
animation-statemenyimpan status saat ini dari tile animasi di atas map sebenarnyasequencesaat iniframesaat inidurationframe saat ini- Waktu
elapsedselama frame saat ini ditampilkan
- Menambahkan dependensi
let-plusagar kode pergantian frame dapat ditulis lebih ringkas - Sistem
update-animationsmenambahelapsedsebesardt, lalu beralih ke frame berikutnya jika durasinya terlampaui- Karena waktu frame bisa lebih kecil daripada
dtyang besar,floordigunakan untuk menghitung berapa frame yang harus dilewati - Dengan
truncate, jika nomor frame melewati panjang daftar, frame akan berputar kembali ke awal - Pointer bitmap pada
imagediganti dengan bitmap dari prefab frame berikutnya
- Karena waktu frame bisa lebih kecil daripada
- Durasi animasi disimpan di Tiled dalam milidetik, sehingga
animation->specmengonversinya ke satuan detik instantiate-animationmembuatanimation-statepada entitas tile sebenarnya, dan menginisialisasielapseddengan angka acak antara 0 dandurationagar animasi yang sama tidak tersinkronisasi sepenuhnya- Tile animasi harus memiliki properti Tiled
"sequence"- Jika properti ini tidak ada, tile akan dimuat dengan nama
NIL, tidak dapat ditemukan dengan nama animasi yang diharapkan, dan dapat menyebabkan error tipe
- Jika properti ini tidak ada, tile akan dimuat dengan nama
Karakter Pemain dan Kontrol
- Menambahkan
character.lispdan mendefinisikan komponencharacteruntuk karakter yang dapat bergerakspeedadalah kecepatan dalam piksel per detiktarget-x,target-yadalah koordinat tujuan gerak- Nilai target awal diisi dengan
single-float-nanagar karakter baru tidak bergerak ke kiri atas tanpa alasan
- Komponen tag
playermenggunakan slotbitdan:index player-entity :unique t- Ini adalah struktur untuk menemukan entitas pemain dalam O(1) dengan
(player-entity 1) - Entitas pemain tidak disimpan dalam variabel global
- Ini adalah struktur untuk menemukan entitas pemain dalam O(1) dengan
- Pada implementasi awal, gambar orc dipotong dari tileset menjadi
player.png, lalu pemain dibuat secara hardcode denganload-player- Posisinya
(64.0, 64.0) - Ukurannya 32×32
- Kecepatannya 100.0
- Posisinya
- Sistem
move-charactersmenggerakkan karakter menuju titik tujuan- Jika ada koordinat target
NaN, koordinat tersebut diinisialisasi ke posisi saat ini - Menggunakan
approx-equal, bukan perbandingan langsung bilangan floating-point - Koordinat baru dihitung dengan
atan,cos,sin, kecepatan, dandt
- Jika ada koordinat target
- Sistem
control-playermembaca input tombolW,A,S,Duntuk memperbarui koordinat target- Menggunakan
al:with-current-keyboard-statedanal:key-down - Menggunakan
clampagar tidak keluar dari batas layar - Dijalankan setelah sistem gerak dengan
:after (move-characters)untuk menghindari masalah inisialisasiNaN
- Menggunakan
Collision dan Pemuatan Objek dengan Properti Tiled
- Awalnya, dinding hanyalah gambar biasa seperti tile lantai, sehingga pemain dapat menembus dinding
- Membuat kelas
map-tilesebagai custom type di Tiled dan menambahkan anggota Booleanobstacle- Menambahkan
map-tilesebagai properti pada tile dinding dan mencentangobstacle
- Menambahkan
- Fungsi
properties->specmengubah hash table properti Tiled menjadi spesifikasi objek ECS- Custom class Tiled diperlakukan sebagai komponen
- Anggota class diperlakukan sebagai slot komponen
- Contohnya berbentuk
((:map-tile :obstacle t))
load-tile-prefabmemasukkan hasilproperties->specke dalam spesifikasi prefab- Jika tidak ada properti, komponen
map-tiledefault ditambahkan denganspec-adjoin, danobstaclememiliki nilai defaultnil
- Jika tidak ada properti, komponen
- Menambahkan slot
tile-hashdan indekstilespada komponenpositiontile-hashmembulatkanxdanyke integer, lalu mengemasnya menjadi satu integer 64-bit- Semua entitas pada koordinat kiri atas tile tertentu ditemukan melalui indeks
tiles
tile-startmengembalikan koordinat kiri atas tile grid yang memuat koordinat sembarangtile-obstacle-pmemeriksa apakah di antara entitas pada koordinat yang sama ada tilemap-tiledenganobstaclebernilai benarobstaclepmemeriksa apakah tile untuk koordinat sembarang merupakan obstaclecontrol-playermemeriksa tile sudut relevan dari persegi karakter sesuai arah gerak, dan jika ada obstacle, koordinat target dikembalikan ke posisi saat ini- Metode collision ini tidak sempurna
- Jika dirancang berdasarkan koordinat pusat karakter, matematika dan kodenya bisa menjadi lebih sederhana, tetapi contoh ini mempertahankan cara saat ini untuk menghindari kompleksitas
Memuat Pemain dan Karakter Animasi dari Map
- Menambahkan custom class
characterdanplayerdi Tiledcharacterhanya memiliki anggota floatspeedtarget-xdantarget-ydihilangkan agar memakai nilai defaultplayermemiliki anggota intplayerdengan nilai default 1
- Menempatkan karakter pemain sebagai objek tile pada layer objek Tiled, lalu memberikan properti
characterdanplayer load-mapdiperluas agar juga memprosestiled:object-layer- Properti objek diubah menjadi komponen ECS dengan
properties->spec tiled:tile-objectmenyalin data tile dan animasi denganload-tile, lalu mengatur posisinya- Karena koordinat objek Tiled berbasis kiri bawah, tinggi objek dikurangkan dari
yagar sesuai dengan basis kiri atas
- Properti objek diubah menjadi komponen ECS dengan
- Pemanggilan dan fungsi
load-playeryang di-hardcode dihapus - Struktur ini membaca data map Tiled langsung sebagai objek ECS, sehingga lebih mendekati data-driven programming
- Animasi karakter menggunakan sequence
orc-idledanorc-runmilik orc yang didefinisikan di tileset change-animation-sequencemengubah animasi saat ini pada entitas- Jika sudah berada pada sequence yang sama, tidak melakukan apa pun
- Frame pertama dari sequence baru dicari melalui indeks
sequence-frames, laluanimation-statedanimage-bitmapdiperbarui
move-charactersberalih ke:orc-idlesaat karakter berhenti, dan ke:orc-runsaat bergerak
Musuh, Game Over, dan Pencarian Jalur A*
- Komponen
enemymemiliki dua slot yang dibutuhkan untuk perilaku musuhvision-range: jarak saat musuh mulai melihat dan bereaksi terhadap pemainattack-range: jangkauan serangan
- Untuk mengakhiri game, variabel global
*should-quit*ditambahkan, dan loop utama berhenti jika nilai ini benar - Sistem
handle-enemiesmengambil koordinat pemain lalu membandingkannya dengan musuh- Jika pemain berada dalam jarak pandang, koordinat tujuan musuh diatur ke posisi pemain
- Jika pemain berada dalam jangkauan serangan,
*should-quit*diatur menjadi benar dan kotak pesan nativeYou diedditampilkan
- Animasi musuh menggunakan sekuens
demon-idledandemon-runmove-charactersmemilih animasi orc untuk pemain dan animasi demon untuk musuh berdasarkan hasilhas-player-p
- Dengan metode pengejaran langsung, musuh juga menembus dinding, sehingga pencarian jalur A* ditambahkan
- cl-astar ditambahkan sebagai dependensi
- Library ini menghasilkan fungsi pencarian jalur yang dioptimalkan untuk masalah tertentu melalui makro
- Jalur tidak disimpan sebagai array di dalam slot komponen; setiap titik jalur direpresentasikan sebagai entitas terpisah
path-pointmemilikix,y, dantraveller, sementaratravellermemiliki indekspath-pointspathmenyimpan tujuan akhirdestination-xdandestination-y- Koordinat tujuan
charactermenunjukkan titik jalur berikutnya, sedangkanpathmenunjukkan tujuan akhir
- Sistem
follow-pathmengambil titik jalur pertama dan memindahkan karakter ke titik tersebut- Saat titik dicapai, entitas
path-pointterkait dihapus - Jika tidak ada titik lagi, komponen
pathdihapus
- Saat titik dicapai, entitas
find-pathdidefinisikan dengana*:define-path-finder- Ukuran dunia dihitung dengan membagi ukuran jendela dengan ukuran tile
- Menggunakan pengindeks row-major
- Ketercapaian tujuan ditentukan berdasarkan apakah koordinat tile sama
- Tetangga dienumerasi dalam 8 arah
- Rintangan atau gerakan diagonal yang melintasi rintangan dibuat praktis mustahil dengan biaya
most-positive-single-float - Heuristik menggunakan octile distance
- Jika sudah ada jalur, titik-titik jalur dihapus lalu
pathbaru dialokasikan - Setiap titik pada jalur hasil dibuat sebagai entitas yang memiliki
path-pointdanparent
handle-enemiesmemanggilfind-pathketika musuh melihat pemain dan tujuan jalur yang ada berbeda dari posisi pemain- Setelah perubahan ini, musuh mengejar pemain sambil menghindari rintangan
UI Game Berbasis Nuklear
- GUI diperlukan untuk elemen naratif, tetapi library GUI tradisional seperti Qt atau GTK tidak cocok untuk UI game yang digambar ke konteks grafis
liballegro - Nuklear digunakan sebagai library UI
- Ada binding Common Lisp cl-liballegro-nuklear untuk digunakan bersama
liballegro - Binding ini juga menyediakan DSL untuk antarmuka deklaratif
- Ada binding Common Lisp cl-liballegro-nuklear untuk digunakan bersama
- Dependensi
cl-liballegro-nuklear/declarativeditambahkan, dansrc/narrative.lispditambahkan sebagai file baru - Nickname lokal
uididaftarkan pada package untuk merujuk singkat kecl-liballegro-nuklear/declarative - Alegreya dari Google Fonts digunakan sebagai font UI, dan nama file diubah menjadi
alegreya-sc.ttf ui:defwindow narrativemendefinisikan fungsi jendela naratif- Posisi jendela dihitung agar berada di area tengah layar
ui:label-wrapmenampilkan teks dengan line wrap otomatisui:button-label "Ok"mengembalikan benar saat diklik
- Nuklear adalah library UI immediate mode
- Alih-alih retained mode yang mempertahankan objek widget di memori, rendering dan pemrosesan dilakukan setiap frame
- Klik tombol diproses bukan melalui callback, melainkan melalui nilai balik dan kondisi di setiap frame
main.lispmemuat font UI dan menginisialisasi konteks UI dengannk:allegro-init- Dalam event loop,
nk:input-begin,nk:allegro-handle-event, dannk:input-enddipanggil - Saat rendering,
nk:allegro-renderdipanggil - Saat keluar,
nk:allegro-shutdowndannk:allegro-font-deldipanggil
- Dalam event loop,
Skin UI dan Objek Naratif
- Karena UI default terasa monoton, styling diterapkan menggunakan aset gambar
fantasy-ui-bordersdari Kenney - Gambar UI disimpan dalam variabel global
*window-background*,*button-normal-background*,*button-hover-background*, dan*button-active-background* load-uimemuat gambar dengannk:allegro-create-image, sedangkanunload-uimelepaskan resource gambar di sisi C dengannk:allegro-del-imageload-uidipanggil diinit, danunload-uidipanggil saat loop utama berakhir- Argumen
:stylespadaui:defwindowmenentukan latar belakang, gambar untuk tiap status tombol, dan warna teks - Komponen
narrativemerepresentasikan objek untuk environmental storytellingtext: teks yang akan ditampilkanshown: apakah sudah pernah ditampilkanactive: apakah jendela saat ini aktifactivememiliki indeksactive-narratives
- Sistem
show-narrativemenampilkan jendela jika pemain berada di dekat objek naratif- Jarak interaksi dihitung dari
+interact-distance-factor+dan ukuran tile pemain - Jendela ditampilkan jika sudah aktif, belum pernah ditampilkan, atau tombol
Editekan - Jendela ditutup dengan salah satu dari tombol
Ok,Esc,Space, atauEnter
- Jarak interaksi dihitung dari
- Di Tiled, tipe kustom
narrativedibuat dan anggota stringtextditambahkan - Agar selaras dengan deteksi tabrakan, objek yang tidak dapat dilewati harus disejajarkan koordinatnya ke grid tile
- Masalah dungeon yang terus bergerak saat jendela naratif tampil dicegah dengan kondisi eksekusi sistem
:when (null (active-narratives t))ditambahkan kemove-charactersdancontrol-player- Jika ada naratif aktif, sistem gerakan dan kontrol tidak dijalankan
- Kondisi kemenangan ditambahkan sebagai komponen tag
win- Jika jendela ditutup pada objek yang juga memiliki
narrative,*should-quit*diatur menjadi benar dan game berakhir
- Jika jendela ditutup pada objek yang juga memiliki
Penutup dan Cakupan
- Contoh akhir menggunakan
cl-fast-ecs,cl-tiled,cl-astar, dancl-liballegro-nuklearuntuk membangun dungeon crawler Souls-like dengan environmental storytelling, AI musuh, dan GUI - Skala implementasinya sekitar 500 baris kode
- Kode lengkap tersedia di repositori GitHub, dan selain kode tutorial juga mencakup deklarasi tipe opsional menggunakan
declaim - Desain suara, cutscene, menu utama, perpindahan level, “door problem”, dan sejenisnya tidak dibahas
- Autumn Lisp Game Jam 2024 berlangsung pada 25 Oktober 2024 di itch.io; dalam acara ini peserta membuat game selama 10 hari dengan dialek Lisp lalu saling menilai dan memberi umpan balik
- Bagian ini didasarkan pada karya peserta Spring Lisp Game Jam 2023, Thoughtbound
- Bagian berikutnya memberi pratinjau tantangan membuat game strategi real-time dengan memperbesar skala dan menambahkan AI yang lebih canggih
1 komentar
Opini Hacker News
Saya berharap semua tutorial teknis bentuknya seperti ini. Strukturnya bagus, hampir tidak ada kesalahan sintaks, setiap kali memperkenalkan topik baru penjelasannya pas porsinya, lengkap dengan contoh kode penuh dan visual yang menunjukkan apa yang sebenarnya dilakukan kode tersebut
Materinya cukup panjang untuk dibahas secara mendalam, tetapi juga cukup mandiri sehingga tetap bisa diikuti meski belum membaca bagian 1 dan hanya pernah mencoba Common Lisp beberapa bulan beberapa tahun lalu. Saya sendiri cukup banyak memakai Clojure dan Emacs Lisp
Bravo, awkravchuk/Andrew :^)
(Crossposted from https://mxjn.me/2024/10/17/1)
Juga mudah disimpan untuk penggunaan offline atau arsip, bisa diberi anotasi, dan mudah dicari
Jarang ada hal di dunia teknis yang lebih menyentuh daripada proyek atau tulisan bagus tentang Common Lisp. Tulisan ini benar-benar seperti hadiah besar
Saya membacanya saat bagian 1 terbit, dan sangat menantikan membaca bagian ini juga. Salut untuk penulisnya
package.shdan keseluruhan manajemen build untuk 3 sistem operasi saja sudah merupakan masterclass tersendiri. Saya belajar banyak hanya dengan menelusuri repositori GitHub-nyaBiasanya saya membangun aplikasi command-line Common Lisp dengan SBCL atau LispWorks, tetapi lain kali mungkin akan mencoba ECL. Keren bisa punya build untuk macOS dan Linux sekaligus, dan sepertinya menyenangkan juga mencoba hal baru
Tulisan yang sangat bagus. Saya sedang mengembangkan shooter multiplayer third-person berbasis mantra dengan Lisp, tepatnya ClojureScript. Ini game 3D berbasis web, dan saya berencana menulis perjalanan ini di blog, termasuk tool dan abstraksi yang saya buat untuk proyek tersebut
Kalau tertarik, demonya ada di sini: https://wizardmasters.io
Tulisannya sendiri benar-benar solid, tetapi melihat proses setup di bagian 1 yang berlapis-lapis—Common Lisp itu sendiri, Python, C, dan seterusnya—rasanya menunjukkan mengapa CL tidak begitu populer, terutama di kalangan programmer muda
Sayang sekali, dan semoga ada yang mau bersusah payah membuat bahasa ini lebih mudah diakses dari sisi instalasi
Sejauh yang saya pahami, fokusnya lebih pada masalah terlalu banyak pilihan dan default lama yang terlihat usang
Event loop adalah contoh bagus yang menunjukkan betapa
loopbenar-benar merupakan domain-specific language untuk iterasi. Suka atau tidak ;)loop? Tidak ada sintaks asing yang bukan S-expression, dan tidak perludountuk kembali ke sintaks LispBisa memakai
if/whenbiasa tanpaelse/endyang mengganggu, dan secara umum juga menambahkan fitur-fitur bergunaloopmenjadi salah satu konstruksi CL favorit sayaTulisan ini mengingatkan saya pada "Caves of Clojure": https://stevelosh.com/blog/2012/07/caves-of-clojure-01/
Kebetulan minggu ini saya mulai mengembangkan roguelike dengan Python, dan sepertinya keren juga kalau dicoba dengan Lisp
Rasanya seperti tertipu. Saya datang untuk belajar cara membuat game sederhana, tetapi malah belajar sangat banyak tentang komputasi secara umum
Benar-benar bagus