- เป็นบทช่วยสอนที่ขยาย สถาปัตยกรรม ECS และการเขียนโปรแกรมเชิงเมตาภาษาบนสภาพแวดล้อมพัฒนาเกมด้วย Common Lisp ไปสู่ตัวอย่างดันเจียนครอว์เลอร์จริง
- อ่านแผนที่ Tiled XML ด้วย cl-tiled แล้วไม่ใช้วัตถุ CLOS โดยตรง แต่ย้ายไปเป็นคอมโพเนนต์ ECS เพื่อแยกการเรนเดอร์ การชน และการจัดการหน่วยความจำออกจากกัน
- ผสาน tile prefab, พอยน์เตอร์รูปภาพ, ดัชนี parent-child และ finalizer เพื่อหลีกเลี่ยง การโหลดซ้ำและ double free พร้อมใช้ custom properties ของ Tiled เหมือนเป็นข้อมูล
- ผู้เล่นและศัตรูทำการเคลื่อนที่ สลับแอนิเมชัน และจัดการการชนผ่านระบบ ECS ส่วนศัตรูไล่ตามโดยหลบกำแพงด้วยการค้นหาเส้นทาง A* บนพื้นฐาน cl-astar
- ทำตัวอย่างดันเจียนครอว์เลอร์ขนาดเล็กประมาณ 500 บรรทัดให้เสร็จ โดยเพิ่ม UI บนพื้นฐาน Nuklear, วัตถุเชิงเรื่องเล่า, การหยุดชั่วคราว และเงื่อนไขชัยชนะ
การเริ่มโปรเจกต์และการรันพื้นฐาน
- สร้างดันเจียนครอว์เลอร์ขนาดเล็กที่มี UI โดยใช้สถาปัตยกรรม Entity-Component-System และเทคนิคการเขียนโปรแกรมเชิงเมตาภาษาที่กล่าวถึงใน Part 1
- ไบนารีเดโมที่รันได้และซอร์สโค้ดอยู่ใน ที่เก็บ GitHub ecs-tutorial-2
- สภาพแวดล้อมการพัฒนาตั้งอยู่บนสภาพแวดล้อมพัฒนาเกม Common Lisp จาก Part 1 และอัปเดตดิสทริบิวชัน Quicklisp ใน SBCL REPL
(ql-util:without-prompting (ql:update-all-dists))
- สร้างโปรเจกต์ใหม่
ecs-tutorial-2ด้วยเทมเพลต cookiecutter-lisp-game และในตัวอย่างเลือกliballegroเป็นแบ็กเอนด์ - เชื่อมไดเรกทอรีโปรเจกต์เข้ากับ
local-projectsของ Quicklisp แล้วเปลี่ยนขนาดหน้าต่างในsrc/main.lispเป็น 1280×800 - เมื่อรัน
(ql:quickload :ecs-tutorial-2)และ(ecs-tutorial-2:main)จะแสดงหน้าต่างสีดำตามความละเอียดที่กำหนดพร้อมตัวนับ FPS
แผนที่ Tiled และที่เก็บ ECS
- การสร้างแผนที่ดันเจียนใช้ตัวแก้ไขแผนที่โอเพนซอร์ส Tiled
- Tiled เป็นเครื่องมือข้ามแพลตฟอร์มและข้ามเอนจิน และบันทึกข้อมูลแผนที่เป็น XML
- ใน Common Lisp cl-tiled จะโหลดไฟล์ Tiled เป็นวัตถุ Lisp
- tileset ตัวอย่างใช้ Dungeon Tileset II - Extended
- เนื่องจากไทล์ต้นฉบับขนาด 16×16 เล็ก จึงใช้ ImageMagick ขยาย 200% ให้เป็นไทล์ 32×32
- สามารถรับ
level1.tmxและไฟล์ tileset ได้จากResources.zipที่บทช่วยสอนจัดเตรียมไว้
- เพิ่ม dependency
cl-tiledในecs-tutorial-2.asdและสร้างsrc/map.lispใหม่เพื่อแยกโค้ดโหลดและแสดงแผนที่ - ใน
src/package.lispลงทะเบียนcl-tiledเป็น local nickname ชื่อtiled
เหตุผลที่ย้ายวัตถุ CLOS ไปเป็นคอมโพเนนต์ ECS
cl-tiledส่งคืนข้อมูลแผนที่เป็น วัตถุ CLOS จึงสำรวจใน REPL ได้สะดวก- หากใช้วัตถุนี้โดยตรงใน game loop อาจมีต้นทุน runtime dispatch สูง
- หากเติมหน้าต่าง 1280×800 ด้วยไทล์ 32×32 ต้องใช้ไทล์อย่างน้อย 40×25 = 1000 ชิ้น
- ในเดโมแยกต่างหาก เมื่อเปิดการเรนเดอร์แผนที่บน Ryzen 5 3600 แบบ 12 คอร์ FPS ลดจาก 20,000 เหลือ 600
- เพิ่มขึ้นประมาณ
1/600 - 1/20000 = 0.0016วินาทีต่อเฟรม หรือมากกว่า 1.5ms
- เมื่อนำข้อมูลที่
cl-tiledอ่านได้ไปไว้ในที่เก็บcl-fast-ecsจะลด dispatch และปรับปรุงการใช้ CPU cache ได้ - เพิ่ม dependency
cl-fast-ecsและเรียกecs:make-storageในinit, เรียกecs:run-systemsในupdate
คอมโพเนนต์แผนที่ ไทล์ และ prefab
mapเป็น tag component ที่แสดงเอนทิตีแผนที่ที่โหลดแล้วmap-tileแทนไทล์แต่ละชิ้น และมีสล็อต Boolean ชื่อobstacleเพื่อระบุว่าเป็นสิ่งกีดขวาง เช่น กำแพงหรือประตูที่ปิดอยู่หรือไม่- คอมโพเนนต์
parentระบุว่าไทล์และวัตถุที่เกี่ยวข้องกับแผนที่เป็นลูกของเอนทิตีแผนที่ใด- ระบุ
:index childrenในสล็อตentityเพื่อค้นหาเอนทิตีลูกของ parent เฉพาะได้อย่างรวดเร็ว - ดัชนีอิง open-address hash table จึงให้การค้นหาเฉลี่ย O(1) แต่มีต้นทุนการอัปเดตเมื่อสร้างหรือลบ
- ระบุ
- เพิ่ม hook ใน
ecs:*entity-deleting-hook*เพื่อให้เมื่อเอนทิตี parent ถูกลบ เอนทิตีลูกที่พบผ่านดัชนีchildrenถูกลบไปด้วย - คอมโพเนนต์
imageเก็บเฉพาะ C pointer ไปยังALLEGRO_BITMAP- รูปภาพ tileset ถูกตัดเป็นชิ้น 32×32 ด้วย
al_create_sub_bitmapแล้วเก็บพอยน์เตอร์ไว้
- รูปภาพ tileset ถูกตัดเป็นชิ้น 32×32 ด้วย
map-tile-prefabเป็น tile prefab ที่มี global IDgidของไทล์ Tiled- กำหนด
:index map-tile-prefab :unique tให้gidเพื่อค้นหาเอนทิตี prefab เดี่ยวตาม ID - ไทล์จริงบนแผนที่จะคัดลอก
imageฯลฯ จาก prefab แต่มีตำแหน่งเป็นคอมโพเนนต์positionแยกต่างหาก
- กำหนด
- finalizer ของ
imageเรียกal_destroy_bitmapเฉพาะเมื่อเอนทิตีเป็นmap-tile-prefab- เพื่อหลีกเลี่ยง double free เพราะไทล์บนแผนที่หลายชิ้นแชร์พอยน์เตอร์
ALLEGRO_BITMAPเดียวกัน
- เพื่อหลีกเลี่ยง double free เพราะไทล์บนแผนที่หลายชิ้นแชร์พอยน์เตอร์
positionและsizeเก็บพิกัดหน้าจอและขนาดเป็นsingle-float- เนื่องจาก
liballegroจัดการพิกัดหน้าจอเป็นเลขทศนิยมความแม่นยำเดี่ยวเพื่อความเข้ากันได้กับ OpenGL จึงใช้แนวทางเดียวกัน
- เนื่องจาก
การเรนเดอร์รูปภาพและการโหลดแผนที่
- ระบบ
render-imagesเรนเดอร์เอนทิตีที่มีpositionและimage- เปิดและปิด sprite batching ด้วย
al_hold_bitmap_drawing - วาดรูปภาพที่พิกัดที่กำหนดด้วย
al_draw_bitmap - prefab ไม่มี
positionจึงไม่ถูกระบบนี้ประมวลผล
- เปิดและปิด sprite batching ด้วย
load-bitmapเป็นฟังก์ชันโหลดรูปภาพที่ห่อal_load_bitmapด้วยal:ensure-loadedtile->specสร้างสเปกวัตถุ ECS สำหรับสร้าง tile prefab- เอนทิตีแผนที่ parent
- ชิ้นส่วนรูปภาพของไทล์
- global tile ID ของ Tiled
- ขนาดไทล์
load-tile-prefabตรวจสอบผ่านดัชนีmap-tile-prefabว่าเป็น prefab ที่โหลดไว้แล้วหรือไม่ และถ้าไม่มีก็สร้างด้วยmake-objectload-tileเมื่อสร้างเอนทิตีไทล์จริงบนแผนที่จะคัดลอกคอมโพเนนต์จาก prefab แล้วเพิ่มpositionload-mapวนผ่าน tileset และเลเยอร์จากวัตถุ CLOS ที่อ่านด้วยtiled:load-map- โหลดรูปภาพ tileset และสร้างแต่ละไทล์เป็น prefab
- สร้างแต่ละเซลล์ของเลเยอร์ไทล์เป็นเอนทิตี แล้วคัดลอกข้อมูล prefab
- ลำดับเลเยอร์ของ Tiled ถูกรักษาตามลำดับใน editor และ
make-entityรับประกันหมายเลขเอนทิตีที่เพิ่มขึ้น- เนื่องจากระบบประมวลผลเอนทิตีเก่าก่อน ไทล์ของเลเยอร์บนจะถูกวาดทีหลังและทับเลเยอร์ล่าง
- วิธีเก็บไทล์ทั้งหมดเป็นเอนทิตีแยกกันไม่ใช่คำตอบเดียวเท่านั้น และสามารถเรนเดอร์แผนที่แบบ static ลงบัฟเฟอร์ไว้ล่วงหน้าได้เช่นกัน
แอนิเมชันไทล์
- Tiled รองรับไทล์แอนิเมชัน จึงสามารถแสดงองค์ประกอบอย่างคบเพลิงหรือน้ำพุเวทมนตร์ได้
- เพิ่ม
common.lispและanimation.lispเพื่อแยกคอมโพเนนต์ทั่วไปออกจากคอมโพเนนต์และระบบที่เกี่ยวกับแอนิเมชัน - คอมโพเนนต์
animation-frameแทนหนึ่งเฟรมของแอนิเมชันsequenceคือชื่อแอนิเมชัน และจัดเก็บเป็นชนิดkeyword- ใช้ดัชนี
sequence-framesเพื่อค้นหาเฟรมต่าง ๆ ของแอนิเมชันที่ระบุ durationคือระยะเวลาของเฟรมในหน่วยวินาที
animation-stateเก็บสถานะปัจจุบันของไทล์แอนิเมชันบนแผนที่จริงsequenceปัจจุบันframeปัจจุบันdurationของเฟรมปัจจุบัน- เวลา
elapsedที่เฟรมปัจจุบันถูกแสดงไปแล้ว
- เพิ่ม dependency
let-plusเพื่อเขียนโค้ดสลับเฟรมให้กระชับ - ระบบ
update-animationsเพิ่มค่าelapsedตามdtและเมื่อเกินระยะเวลา ก็จะสลับไปยังเฟรมถัดไป- เนื่องจากเวลาเฟรมอาจน้อยกว่า
dtที่มีค่ามาก จึงใช้floorคำนวณว่าต้องข้ามไปกี่เฟรม - ใช้
truncateเพื่อให้เมื่อหมายเลขเฟรมเกินความยาวลิสต์แล้ววนกลับไปเริ่มต้น - เปลี่ยนพอยน์เตอร์ bitmap ของ
imageให้เป็น bitmap ของ prefab เฟรมถัดไป
- เนื่องจากเวลาเฟรมอาจน้อยกว่า
- ระยะเวลาแอนิเมชันถูกเก็บใน Tiled เป็นมิลลิวินาที จึงแปลงเป็นหน่วยวินาทีใน
animation->spec instantiate-animationสร้างanimation-stateให้กับเอนทิตีไทล์จริง และกำหนดค่าเริ่มต้นของelapsedเป็นค่าสุ่มระหว่าง 0 ถึงdurationเพื่อไม่ให้แอนิเมชันเดียวกันซิงก์กันสมบูรณ์- ไทล์แอนิเมชันต้องมีพร็อพเพอร์ตี
"sequence"ใน Tiled- หากไม่มีพร็อพเพอร์ตีนี้ จะถูกโหลดด้วยชื่อ
NILทำให้ค้นหาด้วยชื่อแอนิเมชันที่คาดไว้ไม่พบ และอาจเกิด type error ได้
- หากไม่มีพร็อพเพอร์ตีนี้ จะถูกโหลดด้วยชื่อ
ตัวละครผู้เล่นและการควบคุม
- เพิ่ม
character.lispและนิยามคอมโพเนนต์characterสำหรับตัวละครที่เคลื่อนที่ได้speedคือความเร็วเป็นพิกเซลต่อวินาทีtarget-x,target-yคือพิกัดเป้าหมายการเคลื่อนที่- ค่าเป้าหมายเริ่มต้นตั้งเป็น
single-float-nanเพื่อไม่ให้ตัวละครใหม่เคลื่อนที่ไปมุมซ้ายบนโดยไม่มีเหตุผล
- คอมโพเนนต์แท็ก
playerใช้สล็อตbitและ:index player-entity :unique t- เป็นโครงสร้างสำหรับหาเอนทิตีผู้เล่นด้วย
(player-entity 1)ในเวลา O(1) - ไม่เก็บเอนทิตีผู้เล่นไว้ในตัวแปร global
- เป็นโครงสร้างสำหรับหาเอนทิตีผู้เล่นด้วย
- ในการใช้งานครั้งแรก ตัดภาพออร์กจาก tileset มาทำเป็น
player.pngและสร้างผู้เล่นแบบ hardcode ด้วยload-player- ตำแหน่งคือ
(64.0, 64.0) - ขนาดคือ 32×32
- ความเร็วคือ 100.0
- ตำแหน่งคือ
- ระบบ
move-charactersเคลื่อนตัวละครไปยังจุดเป้าหมาย- หากมีพิกัดเป้าหมายเป็น
NaNจะกำหนดค่าเริ่มต้นเป็นตำแหน่งปัจจุบัน - ใช้
approx-equalแทนการเปรียบเทียบ floating point โดยตรง - คำนวณพิกัดใหม่ด้วย
atan,cos,sin, ความเร็ว และdt
- หากมีพิกัดเป้าหมายเป็น
- ระบบ
control-playerอ่านอินพุตปุ่มW,A,S,Dแล้วอัปเดตพิกัดเป้าหมาย- ใช้
al:with-current-keyboard-stateและal:key-down - ใช้
clampเพื่อไม่ให้ออกนอกขอบหน้าจอ - รันตามหลังระบบเคลื่อนที่ด้วย
:after (move-characters)เพื่อหลีกเลี่ยงปัญหาการกำหนดค่าเริ่มต้นNaN
- ใช้
การโหลดการชนและออบเจ็กต์ด้วยพร็อพเพอร์ตีของ Tiled
- ตอนแรกกำแพงเป็นภาพทั่วไปเหมือนไทล์พื้น ทำให้ผู้เล่นเดินทะลุกำแพงได้
- สร้างคลาส
map-tileเป็น custom type ของ Tiled และเพิ่มสมาชิก Boolean ชื่อobstacle- เพิ่ม
map-tileเป็นพร็อพเพอร์ตีของไทล์กำแพง และติ๊กobstacle
- เพิ่ม
- ฟังก์ชัน
properties->specแปลง hash table ของพร็อพเพอร์ตี Tiled เป็นสเปกออบเจ็กต์ ECS- custom class ของ Tiled ถูกจัดการเป็นคอมโพเนนต์
- สมาชิกของคลาสถูกจัดการเป็นสล็อตของคอมโพเนนต์
- ตัวอย่างอยู่ในรูป
((:map-tile :obstacle t))
load-tile-prefabรวมผลลัพธ์จากproperties->specเข้าไปในสเปก prefab- หากไม่มีพร็อพเพอร์ตี จะเพิ่มคอมโพเนนต์
map-tileพื้นฐานด้วยspec-adjoinและobstacleมีค่าเริ่มต้นเป็นnil
- หากไม่มีพร็อพเพอร์ตี จะเพิ่มคอมโพเนนต์
- เพิ่มสล็อต
tile-hashและดัชนีtilesให้คอมโพเนนต์positiontile-hashทำให้x,yเป็นจำนวนเต็ม แล้วแพ็กเป็นจำนวนเต็ม 64 บิตค่าเดียว- ค้นหาเอนทิตีทั้งหมดที่อยู่บนพิกัดมุมซ้ายบนของไทล์ที่ระบุด้วยดัชนี
tiles
tile-startคืนค่าพิกัดมุมซ้ายบนของไทล์ในกริดที่พิกัดใด ๆ สังกัดอยู่tile-obstacle-pตรวจว่ามีไทล์ที่เป็นmap-tileและมีobstacleเป็นจริงในหมู่เอนทิตีที่พิกัดเดียวกันหรือไม่obstaclepตรวจว่าไทล์นั้นเป็นสิ่งกีดขวางหรือไม่สำหรับพิกัดใด ๆcontrol-playerตรวจไทล์ที่มุมที่เกี่ยวข้องของสี่เหลี่ยมตัวละครตามทิศทางการเคลื่อนที่ และถ้ามีสิ่งกีดขวางก็ย้อนพิกัดเป้าหมายกลับเป็นตำแหน่งปัจจุบัน- วิธีการชนนี้ยังไม่สมบูรณ์
- หากออกแบบโดยอิงพิกัดศูนย์กลางของตัวละคร คณิตศาสตร์และโค้ดอาจเรียบง่ายขึ้นได้ แต่ตัวอย่างนี้คงวิธีปัจจุบันไว้เพื่อหลีกเลี่ยงความซับซ้อน
การโหลดผู้เล่นและตัวละครแอนิเมชันจากแผนที่
- เพิ่ม custom class
characterและplayerใน Tiledcharacterมีเฉพาะสมาชิก float ชื่อspeed- ละ
target-x,target-yไว้เพื่อให้ใช้ค่าเริ่มต้น playerมีสมาชิก int ชื่อplayerที่ค่าเริ่มต้นเป็น 1
- วางตัวละครผู้เล่นเป็น tile object ใน object layer ของ Tiled และกำหนดพร็อพเพอร์ตี
characterกับplayer - ขยาย
load-mapให้รองรับtiled:object-layerด้วย- แปลงพร็อพเพอร์ตีของออบเจ็กต์เป็นคอมโพเนนต์ ECS ด้วย
properties->spec tiled:tile-objectคัดลอกข้อมูลไทล์และแอนิเมชันด้วยload-tileแล้วตั้งตำแหน่ง- พิกัดออบเจ็กต์ของ Tiled อ้างอิงจากมุมซ้ายล่าง จึงลบความสูงของออบเจ็กต์ออกจาก
yเพื่อปรับให้เป็นการอ้างอิงจากมุมซ้ายบน
- แปลงพร็อพเพอร์ตีของออบเจ็กต์เป็นคอมโพเนนต์ ECS ด้วย
- ลบการเรียกและฟังก์ชัน
load-playerที่ hardcode ไว้ - โครงสร้างนี้เป็นวิธีอ่านข้อมูลแผนที่ Tiled เข้ามาเป็นออบเจ็กต์ ECS โดยตรง และเข้าใกล้การเขียนโปรแกรมแบบ data-driven มากขึ้น
- แอนิเมชันตัวละครใช้ sequence
orc-idleและorc-runของออร์กที่นิยามไว้ใน tileset change-animation-sequenceเปลี่ยนแอนิเมชันปัจจุบันของเอนทิตี- หากเป็น sequence เดิมอยู่แล้วจะไม่ทำอะไร
- ค้นหาเฟรมแรกของ sequence ใหม่ด้วยดัชนี
sequence-framesแล้วอัปเดตanimation-stateและimage-bitmap
move-charactersสลับเป็น:orc-idleเมื่อตัวละครหยุดอยู่ และเป็น:orc-runเมื่อเคลื่อนที่
ศัตรู, เกมโอเวอร์, การค้นหาเส้นทาง A*
- คอมโพเนนต์
enemyมีสล็อตสองตัวที่จำเป็นต่อพฤติกรรมของศัตรูvision-range: ระยะที่ศัตรูมองเห็นผู้เล่นและเริ่มตอบสนองattack-range: ระยะโจมตี
- เพิ่มตัวแปรโกลบอล
*should-quit*สำหรับจบเกม และลูปหลักจะสิ้นสุดเมื่อค่านี้เป็นจริง - ระบบ
handle-enemiesจะดึงพิกัดผู้เล่นมาเปรียบเทียบกับศัตรู- หากผู้เล่นอยู่ในระยะมองเห็น จะตั้งพิกัดเป้าหมายของศัตรูเป็นตำแหน่งของผู้เล่น
- หากผู้เล่นอยู่ในระยะโจมตี จะตั้ง
*should-quit*เป็นจริง และแสดงกล่องข้อความเนทีฟYou died
- แอนิเมชันของศัตรูใช้ซีเควนซ์
demon-idle,demon-runmove-charactersจะเลือกแอนิเมชันออร์กสำหรับผู้เล่น และแอนิเมชันเดมอนสำหรับศัตรู ตามผลลัพธ์ของhas-player-p
- ในวิธีไล่ตามโดยตรง ศัตรูก็จะเดินทะลุกำแพงด้วย จึงเพิ่มการค้นหาเส้นทาง A*
- เพิ่ม cl-astar เป็น dependency
- ไลบรารีนี้สร้างฟังก์ชันค้นหาเส้นทางที่ปรับแต่งให้เหมาะกับปัญหาด้วยแมโคร
- เส้นทางไม่ได้เก็บเป็นอาร์เรย์ในสล็อตของคอมโพเนนต์ แต่แสดงจุดแต่ละจุดของเส้นทางเป็นเอนทิตีแยกต่างหาก
path-pointมีx,y,travellerและในtravellerมีอินเด็กซ์path-pointspathเก็บปลายทางสุดท้ายdestination-x,destination-y- พิกัดเป้าหมายของ
characterคือจุดเส้นทางถัดไป ส่วนpathแทนปลายทางสุดท้าย
- ระบบ
follow-pathจะดึงจุดเส้นทางแรกมาและย้ายตัวละครไปยังจุดนั้น- เมื่อไปถึงจุดแล้ว จะลบเอนทิตี
path-pointนั้น - หากไม่มีจุดเหลือแล้ว จะลบคอมโพเนนต์
path
- เมื่อไปถึงจุดแล้ว จะลบเอนทิตี
find-pathถูกนิยามด้วยa*:define-path-finder- คำนวณขนาดโลกโดยหารขนาดหน้าต่างด้วยขนาดไทล์
- ใช้ตัวทำดัชนีแบบ row-major
- ตัดสินว่าถึงเป้าหมายหรือไม่จากการที่พิกัดไทล์เหมือนกัน
- ไล่แจกแจงเพื่อนบ้านใน 8 ทิศทาง
- ทำให้สิ่งกีดขวางหรือการเคลื่อนที่แนวทแยงที่ตัดผ่านสิ่งกีดขวางแทบเป็นไปไม่ได้ โดยกำหนดต้นทุนเป็น
most-positive-single-float - heuristic ใช้ octile distance
- หากมีเส้นทางเดิมอยู่ จะลบจุดเส้นทางเหล่านั้นและจัดสรร
pathใหม่ - แต่ละจุดของเส้นทางผลลัพธ์จะถูกสร้างเป็นเอนทิตีที่มี
path-pointและparent
- เมื่อศัตรูมองเห็นผู้เล่น
handle-enemiesจะเรียกfind-pathหากปลายทางของเส้นทางเดิมต่างจากตำแหน่งผู้เล่น - หลังการเปลี่ยนแปลง ศัตรูจะไล่ตามผู้เล่นแต่เคลื่อนที่หลบสิ่งกีดขวาง
UI เกมที่ใช้ Nuklear
- ต้องมี GUI สำหรับองค์ประกอบเชิงเนื้อเรื่อง แต่ไลบรารี GUI แบบดั้งเดิมอย่าง Qt หรือ GTK ไม่เหมาะกับ UI เกมที่วาดลงในกราฟิกคอนเท็กซ์ของ
liballegro - ใช้ Nuklear เป็นไลบรารี UI
- มี Common Lisp binding cl-liballegro-nuklear สำหรับใช้ร่วมกับ
liballegro - binding นี้ยังมี DSL สำหรับอินเทอร์เฟซแบบ declarative ด้วย
- มี Common Lisp binding cl-liballegro-nuklear สำหรับใช้ร่วมกับ
- เพิ่ม dependency
cl-liballegro-nuklear/declarativeและเพิ่มsrc/narrative.lispเป็นไฟล์ใหม่ - ลงทะเบียน local nickname
uiในแพ็กเกจ เพื่ออ้างถึงcl-liballegro-nuklear/declarativeแบบสั้น ๆ - ใช้ Alegreya จาก Google Fonts เป็นฟอนต์ UI และเปลี่ยนชื่อไฟล์เป็น
alegreya-sc.ttf ui:defwindow narrativeนิยามฟังก์ชันหน้าต่างเนื้อเรื่อง- คำนวณตำแหน่งหน้าต่างให้อยู่บริเวณกึ่งกลางหน้าจอ
- แสดงข้อความที่ตัดบรรทัดอัตโนมัติด้วย
ui:label-wrap ui:button-label "Ok"จะคืนค่าเป็นจริงเมื่อถูกคลิก
- Nuklear เป็นไลบรารี UI แบบ immediate mode
- ไม่ใช่ retained mode ที่คงอ็อบเจ็กต์วิดเจ็ตไว้ในหน่วยความจำ แต่ทำการเรนเดอร์และประมวลผลทุกเฟรม
- การคลิกปุ่มไม่ได้จัดการด้วย callback แต่จัดการด้วยค่าที่คืนมาและเงื่อนไขในแต่ละเฟรม
main.lispโหลดฟอนต์ UI และเริ่มต้น UI context ด้วยnk:allegro-init- ใน event loop จะเรียก
nk:input-begin,nk:allegro-handle-event,nk:input-end - ตอนเรนเดอร์จะเรียก
nk:allegro-render - ตอนปิดจะเรียก
nk:allegro-shutdown,nk:allegro-font-del
- ใน event loop จะเรียก
สกิน UI และอ็อบเจ็กต์เนื้อเรื่อง
- เนื่องจาก UI พื้นฐานดูเรียบเกินไป จึงใช้แอสเซ็ตรูปภาพ
fantasy-ui-bordersของ Kenney มาใส่สไตล์ - เก็บรูปภาพ UI ไว้ในตัวแปรโกลบอล
*window-background*,*button-normal-background*,*button-hover-background*,*button-active-background* load-uiโหลดรูปภาพด้วยnk:allegro-create-imageและunload-uiปล่อยทรัพยากรรูปภาพฝั่ง C ด้วยnk:allegro-del-image- เรียก
load-uiในinitและเรียกunload-uiเมื่อสิ้นสุดลูปหลัก - ระบุพื้นหลัง รูปภาพตามสถานะของปุ่ม และสีข้อความผ่านอาร์กิวเมนต์
:stylesของui:defwindow - คอมโพเนนต์
narrativeแทนอ็อบเจ็กต์สำหรับการเล่าเรื่องผ่านสภาพแวดล้อมtext: ข้อความที่จะแสดงshown: ระบุว่าเคยแสดงไปแล้วหนึ่งครั้งหรือไม่active: ระบุว่าหน้าต่างกำลังแอ็กทีฟอยู่หรือไม่activeมีอินเด็กซ์active-narratives
- ระบบ
show-narrativeจะแสดงหน้าต่างเมื่อผู้เล่นอยู่ใกล้อ็อบเจ็กต์เนื้อเรื่อง- ระยะโต้ตอบคำนวณจาก
+interact-distance-factor+และขนาดไทล์ของผู้เล่น - แสดงหน้าต่างหากหน้าต่างแอ็กทีฟอยู่แล้ว, ยังไม่เคยแสดงมาก่อน, หรือกดปุ่ม
E - ปิดหน้าต่างด้วยปุ่ม
Ok,Esc,SpaceหรือEnterอย่างใดอย่างหนึ่ง
- ระยะโต้ตอบคำนวณจาก
- ใน Tiled สร้างชนิดกำหนดเอง
narrativeและเพิ่มสมาชิก string ชื่อtext - เพื่อให้ตรงกับการตรวจจับการชน อ็อบเจ็กต์ที่เดินผ่านไม่ได้ต้องจัดพิกัดให้ตรงกับกริดไทล์
- ปัญหาที่ดันเจียนยังคงเคลื่อนไหวขณะที่หน้าต่างเนื้อเรื่องเปิดอยู่ ถูกป้องกันด้วยเงื่อนไขการรันของระบบ
- เพิ่ม
:when (null (active-narratives t))ให้กับmove-charactersและcontrol-player - หากมีเนื้อเรื่องที่แอ็กทีฟอยู่ ระบบเคลื่อนที่และควบคุมจะไม่ถูกเรียกใช้
- เพิ่ม
- เพิ่มเงื่อนไขชนะด้วยคอมโพเนนต์แท็ก
win- เมื่อปิดหน้าต่างจากอ็อบเจ็กต์ที่ติด
narrativeร่วมอยู่ด้วย จะตั้ง*should-quit*เป็นจริงและจบเกม
- เมื่อปิดหน้าต่างจากอ็อบเจ็กต์ที่ติด
สรุปและขอบเขต
- ตัวอย่างสุดท้ายใช้
cl-fast-ecs,cl-tiled,cl-astar,cl-liballegro-nuklearเพื่อสร้างดันเจียนครอว์เลอร์สไตล์ Souls-like ที่มีการเล่าเรื่องผ่านสภาพแวดล้อม, AI ศัตรู และ GUI - ขนาดการใช้งานอยู่ที่ประมาณ 500 บรรทัดโค้ด
- โค้ดทั้งหมดอยู่ใน GitHub repository และนอกจากโค้ดบทเรียนแล้วยังมีการประกาศชนิดแบบเลือกใช้ด้วย
declaim - ไม่ครอบคลุม sound design, cutscene, main menu, level transition, “door problem” และอื่น ๆ
- Autumn Lisp Game Jam 2024 จะจัดขึ้นที่ itch.io วันที่ 25 ตุลาคม 2024 เป็นงานที่ให้สร้างเกมด้วยภาษาในตระกูล Lisp เป็นเวลา 10 วัน แล้วประเมินและให้ฟีดแบ็กกันและกัน
- ส่วนนี้อิงจากผลงาน Thoughtbound ที่ส่งเข้าประกวด Spring Lisp Game Jam 2023
- ตอนถัดไปเกริ่นว่าจะท้าทายด้วยการขยายสเกลและเพิ่ม AI ขั้นสูงขึ้นเพื่อสร้างเกมกลยุทธ์แบบเรียลไทม์
1 ความคิดเห็น
ความเห็นจาก Hacker News
อยากให้บทสอนเทคนิคทุกชิ้นเป็นแบบนี้ โครงสร้างบทความดีมาก แทบไม่มีข้อผิดพลาดทางไวยากรณ์เลย และทุกครั้งที่แนะนำหัวข้อใหม่ก็อธิบายในปริมาณที่พอดี พร้อมทั้งมี ตัวอย่างโค้ดฉบับเต็ม และ สื่อภาพ ที่แสดงให้เห็นว่าโค้ดทำอะไรจริง ๆ
มันยาวพอที่จะลงลึกกับเนื้อหา แต่ก็เป็นงานที่อ่านแยกเดี่ยวได้ดีพอที่แม้จะไม่ได้อ่านภาคแรก และเคยลองจับ Common Lisp แค่ไม่กี่เดือนเมื่อหลายปีก่อนก็ยังตามได้ ผมเองเคยใช้ Clojure และ Emacs Lisp ค่อนข้างมาก
Bravo, awkravchuk/Andrew :^)
(Crossposted from https://mxjn.me/2024/10/17/1)
อีกทั้งยังบันทึกเก็บไว้ใช้แบบออฟไลน์หรือเก็บถาวรได้ง่าย ใส่คำอธิบายประกอบได้ และค้นหาก็สะดวก
มีไม่กี่อย่างในสายเทคนิคที่ทำให้รู้สึกประทับใจได้เท่ากับโปรเจกต์หรือบทความ Common Lisp ดี ๆ นี่เป็นงานเขียนที่เหมือนของขวัญชิ้นใหญ่จริง ๆ
ผมอ่านภาคแรกตอนที่ออกมา และตื่นเต้นมากที่จะได้อ่านภาคนี้ด้วย ขอชื่นชมผู้เขียนอย่างยิ่ง
แค่
package.shกับการจัดการ การบิลด์ข้าม 3 ระบบปฏิบัติการ โดยรวมก็ถือเป็นมาสเตอร์คลาสในตัวเองแล้ว แค่ไล่ดู GitHub repository ก็ได้เรียนรู้อะไรไปเยอะมากปกติผมมักบิลด์แอปบรรทัดคำสั่ง Common Lisp ด้วย SBCL หรือ LispWorks แต่ครั้งหน้าอาจลอง ECL ดูบ้าง การมีทั้งบิลด์สำหรับ macOS และ Linux นั้นเจ๋งดี และก็น่าจะสนุกที่ได้ลองอะไรใหม่ ๆ
บทความดีมาก ผมกำลังพัฒนา เกมยิงมุมมองบุคคลที่สามแบบหลายผู้เล่นที่ใช้เวทมนตร์เป็นหลัก ด้วย Lisp หรือจะให้แม่นกว่านั้นคือ ClojureScript เป็นเกม 3D บนเว็บ และผมก็ตั้งใจจะเขียนบล็อกเล่าเส้นทางนี้รวมถึงเครื่องมือและ abstraction ที่ทำขึ้นสำหรับโปรเจกต์นี้ด้วย
ถ้าสนใจ เดโมอยู่ที่นี่: https://wizardmasters.io
ตัวบทความเองแข็งแรงมาก แต่พอเห็นขั้นตอนเซ็ตอัปในภาคแรกที่โยงไปทั้ง Common Lisp เอง, Python, C และอีกหลายสเต็ป ก็พอจะเห็นได้ว่าทำไม CL ถึงไม่ได้เป็นที่นิยมมากนัก โดยเฉพาะในหมู่นักเขียนโปรแกรมรุ่นใหม่
น่าเสียดาย และคงดีถ้ามีใครสักคนช่วยทุ่มเทเพื่อ ทำให้ภาษาเข้าถึงง่ายขึ้นในแง่การติดตั้ง
เท่าที่เข้าใจ มันโฟกัสมากกว่าที่ปัญหาเรื่องมีตัวเลือกเยอะเกินไป และค่าเริ่มต้นเก่า ๆ ที่ดูเชยแล้ว
event loop เป็นตัวอย่างที่ยอดเยี่ยมว่า
loopนั้นจริงจังแค่ไหนในฐานะ ภาษาย่อยเฉพาะทางสำหรับการวนซ้ำ จะชอบหรือไม่ชอบก็ตาม ;)loopไม่ได้หรือ? จะได้ไม่มีไวยากรณ์ประหลาดที่ไม่ใช่ S-expression และก็ไม่ต้องมีdoเพื่อย้อนกลับไปใช้ไวยากรณ์แบบ Lispใช้
if/whenปกติได้โดยไม่มีelse/endที่ชวนปวดตา และโดยรวมก็เพิ่มความสามารถที่มีประโยชน์เข้าไปด้วยloopกลับกลายเป็นหนึ่งในองค์ประกอบของ CL ที่ผมชอบที่สุดบทความนี้ทำให้นึกถึง "Caves of Clojure": https://stevelosh.com/blog/2012/07/caves-of-clojure-01/
บังเอิญว่าผมเพิ่งเริ่มพัฒนา roguelike ด้วย Python ในสัปดาห์นี้พอดี ลองทำด้วย Lisp ก็น่าจะเท่ดี
รู้สึกเหมือนโดนหลอกเลย ผมเข้ามาเพื่อเรียนรู้วิธีทำเกมง่าย ๆ แต่กลับได้เรียนรู้เรื่อง การคำนวณโดยรวมทั้งวงการ ไปมหาศาล
ดีมากจริง ๆ