2 điểm bởi GN⁺ 2024-10-19 | 1 bình luận | Chia sẻ qua WhatsApp
  • Đây là bài hướng dẫn mở rộng kiến trúc ECS và lập trình siêu ngôn ngữ trên môi trường phát triển game Common Lisp bằng một ví dụ dungeon crawler thực tế
  • Sau khi đọc bản đồ XML của Tiled bằng cl-tiled, dữ liệu được chuyển sang các component ECS thay vì dùng trực tiếp đối tượng CLOS, qua đó tách riêng việc render, va chạm và quản lý bộ nhớ
  • Kết hợp tile prefab, con trỏ hình ảnh, chỉ mục cha-con và finalizer để tránh tải trùng lặp và double free, đồng thời tận dụng thuộc tính tùy biến của Tiled như dữ liệu
  • Người chơi và kẻ địch được xử lý bằng các hệ thống ECS để di chuyển, chuyển hoạt ảnh và xử lý va chạm; kẻ địch truy đuổi bằng tìm đường A* dựa trên cl-astar để né tường
  • Hoàn thiện một ví dụ dungeon crawler nhỏ khoảng 500 dòng với UI dựa trên Nuklear, đối tượng tường thuật, tạm dừng và cả điều kiện chiến thắng

Bắt đầu dự án và chạy cơ bản

  • Sử dụng kiến trúc Entity-Component-System và các kỹ thuật lập trình siêu ngôn ngữ đã đề cập ở Part 1 để tạo một dungeon crawler nhỏ có UI
  • Demo binary có thể chạy và mã nguồn có tại ecs-tutorial-2 GitHub repository
  • Môi trường phát triển giả định là môi trường phát triển game Common Lisp ở Part 1, và cập nhật bản phân phối Quicklisp trong SBCL REPL
    • (ql-util:without-prompting (ql:update-all-dists))
  • Tạo dự án mới ecs-tutorial-2 bằng mẫu cookiecutter-lisp-game, và trong ví dụ chọn liballegro làm backend
  • Liên kết thư mục dự án vào Quicklisp local-projects, sau đó đổi kích thước cửa sổ trong src/main.lisp thành 1280×800
  • Chạy (ql:quickload :ecs-tutorial-2)(ecs-tutorial-2:main) sẽ hiển thị một cửa sổ đen ở độ phân giải đã chỉ định cùng bộ đếm FPS

Bản đồ Tiled và kho lưu trữ ECS

  • Sử dụng trình biên tập bản đồ mã nguồn mở Tiled để tạo bản đồ dungeon
    • Tiled là công cụ đa nền tảng, đa engine và lưu dữ liệu bản đồ ở dạng XML
    • Trong Common Lisp, cl-tiled tải file Tiled thành các đối tượng Lisp
  • Bộ tileset ví dụ sử dụng Dungeon Tileset II - Extended
    • Vì các tile gốc 16×16 quá nhỏ, chúng được phóng to 200% bằng ImageMagick để dùng thành tile 32×32
    • level1.tmx và file tileset có thể tải trong Resources.zip do bài hướng dẫn cung cấp
  • Thêm phụ thuộc cl-tiled vào ecs-tutorial-2.asd, tạo mới src/map.lisp để tách riêng mã tải và hiển thị bản đồ
  • Trong src/package.lisp, đăng ký cl-tiled với biệt danh cục bộ là tiled

Vì sao chuyển đối tượng CLOS sang component ECS

  • cl-tiled trả về dữ liệu bản đồ dưới dạng đối tượng CLOS, nên rất thuận tiện để khám phá trong REPL
  • Nhưng nếu dùng trực tiếp các đối tượng này trong vòng lặp game, chi phí dispatch khi chạy có thể tăng cao
    • Một cửa sổ 1280×800 lấp đầy bằng tile 32×32 sẽ cần ít nhất 40×25 = 1000 tile
    • Trong một demo riêng, khi bật render bản đồ trên Ryzen 5 3600 12 lõi, FPS giảm từ 20.000 xuống 600
    • Tức là tăng thêm khoảng 1/600 - 1/20000 = 0.0016 giây mỗi frame, hay hơn 1,5ms
  • Nếu chuyển dữ liệu do cl-tiled đọc sang kho lưu trữ cl-fast-ecs, có thể giảm dispatch và cải thiện khả năng tận dụng CPU cache
  • Thêm phụ thuộc cl-fast-ecs, rồi gọi ecs:make-storage trong initecs:run-systems trong update

Component bản đồ, tile và prefab

  • maptag component biểu thị entity bản đồ đã được nạp
  • map-tile biểu thị từng tile riêng lẻ và có slot Boolean obstacle để đánh dấu vật cản như tường hoặc cửa đóng
  • Component parent cho biết tile và các đối tượng liên quan đến bản đồ là con của entity bản đồ nào
    • Chỉ định :index children cho slot entity để tìm nhanh các entity con của một parent cụ thể
    • Chỉ mục này dựa trên bảng băm open-address, cung cấp tra cứu trung bình O(1) nhưng có chi phí cập nhật khi tạo hoặc xóa
  • Thêm hook vào ecs:*entity-deleting-hook* để khi xóa entity cha thì các entity con tìm được qua chỉ mục children cũng bị xóa theo
  • Component image chỉ lưu con trỏ C trỏ tới ALLEGRO_BITMAP
    • Hình ảnh tileset được cắt thành các mảnh 32×32 bằng al_create_sub_bitmap rồi lưu con trỏ
  • map-tile-prefab là prefab tile mang ID toàn cục gid của tile trong Tiled
    • Với gid, chỉ định :index map-tile-prefab :unique t để tìm một entity prefab duy nhất theo ID
    • Các tile thực trên bản đồ sẽ sao chép image v.v. từ prefab, còn vị trí được giữ riêng trong component position
  • Finalizer của image chỉ gọi al_destroy_bitmap khi entity là map-tile-prefab
    • Mục đích là tránh double free vì nhiều tile trên bản đồ cùng chia sẻ một con trỏ ALLEGRO_BITMAP
  • positionsize lưu tọa độ màn hình và kích thước dưới dạng single-float
    • liballegro xử lý tọa độ màn hình bằng số thực dấu chấm động đơn để tương thích OpenGL, nên ở đây cũng làm tương tự

Render hình ảnh và tải bản đồ

  • Hệ thống render-images render các entity có positionimage
    • Bật và tắt sprite batching bằng al_hold_bitmap_drawing
    • Vẽ hình tại tọa độ chỉ định bằng al_draw_bitmap
    • Vì prefab không có position, chúng không được xử lý trong hệ thống này
  • load-bitmap là hàm tải ảnh bọc al_load_bitmap bằng al:ensure-loaded
  • tile->spec tạo đặc tả đối tượng ECS để sinh prefab tile
    • Entity bản đồ cha
    • Mảnh hình ảnh của tile
    • ID tile toàn cục của Tiled
    • Kích thước tile
  • load-tile-prefab kiểm tra prefab đã được nạp hay chưa qua chỉ mục map-tile-prefab, nếu chưa thì tạo bằng make-object
  • load-tile khi tạo entity tile thực trên bản đồ sẽ sao chép component từ prefab và thêm position
  • load-map duyệt tileset và layer từ đối tượng CLOS được đọc bằng tiled:load-map
    • Tải hình tileset và tạo prefab cho từng tile
    • Tạo entity cho từng ô trong tile layer và sao chép dữ liệu prefab
  • Thứ tự layer của Tiled được giữ nguyên theo thứ tự trong editor, và make-entity đảm bảo số entity tăng dần
    • Vì hệ thống xử lý các entity cũ trước, tile của layer phía trên sẽ được vẽ sau và che layer phía dưới
  • Cách lưu mọi tile thành entity riêng không phải là đáp án duy nhất; cũng có thể render trước bản đồ tĩnh vào một buffer

Hoạt ảnh tile

  • Tiled hỗ trợ tile hoạt ảnh, nên có thể biểu diễn các yếu tố như đuốc hoặc đài phun ma thuật
  • Thêm common.lispanimation.lisp để tách các component dùng chung khỏi các component và system liên quan đến hoạt ảnh
  • Component animation-frame biểu thị một frame của hoạt ảnh
    • sequence là tên hoạt ảnh và được lưu dưới kiểu keyword
    • Dùng chỉ mục sequence-frames để tìm các frame của một hoạt ảnh cụ thể
    • duration là thời lượng hiển thị frame tính bằng giây
  • animation-state lưu trạng thái hiện tại của tile hoạt ảnh thực tế trên bản đồ
    • sequence hiện tại
    • frame hiện tại
    • duration của frame hiện tại
    • Thời gian elapsed mà frame hiện tại đã được hiển thị
  • Thêm dependency let-plus để viết mã chuyển frame gọn hơn
  • System update-animations tăng elapsed lên theo dt, và khi vượt quá thời lượng thì chuyển sang frame tiếp theo
    • Vì thời gian frame có thể nhỏ hơn dt lớn, dùng floor để tính cần bỏ qua bao nhiêu frame
    • Dùng truncate để khi số frame vượt quá độ dài danh sách thì quay vòng về đầu
    • Đổi con trỏ bitmap của image sang bitmap của prefab frame kế tiếp
  • Thời lượng hoạt ảnh được Tiled lưu theo mili giây, nên trong animation->spec sẽ được đổi sang đơn vị giây
  • instantiate-animation tạo animation-state cho tile entity thực tế, đồng thời khởi tạo elapsed bằng một số ngẫu nhiên giữa 0 và duration để các hoạt ảnh giống nhau không đồng bộ hoàn toàn
  • Tile hoạt ảnh phải có thuộc tính Tiled "sequence"
    • Nếu thiếu thuộc tính này, nó sẽ được tải với tên NIL, không thể tìm bằng tên hoạt ảnh như mong đợi và có thể gây lỗi kiểu dữ liệu

Nhân vật người chơi và điều khiển

  • Thêm character.lisp và định nghĩa component character cho các nhân vật có thể di chuyển
    • speed là tốc độ tính theo pixel mỗi giây
    • target-x, target-y là tọa độ đích di chuyển
    • Giá trị đích ban đầu được đặt là single-float-nan để nhân vật mới không tự nhiên di chuyển về góc trên bên trái
  • Component tag player dùng slot bit:index player-entity :unique t
    • Đây là cấu trúc để tìm player entity trong O(1) bằng (player-entity 1)
    • Không lưu player entity trong biến toàn cục
  • Ở bản cài đặt ban đầu, ảnh orc được cắt từ tileset thành player.png, rồi tạo người chơi bằng load-player theo kiểu hardcode
    • Vị trí là (64.0, 64.0)
    • Kích thước là 32×32
    • Tốc độ là 100.0
  • System move-characters di chuyển nhân vật tới điểm đích
    • Nếu tọa độ đích là NaN thì khởi tạo bằng vị trí hiện tại
    • Dùng approx-equal thay vì so sánh trực tiếp số thực dấu phẩy động
    • Tính tọa độ mới bằng atan, cos, sin, tốc độ và dt
  • System control-player đọc input phím W, A, S, D để cập nhật tọa độ đích
    • Dùng al:with-current-keyboard-stateal:key-down
    • Dùng clamp để không đi ra ngoài biên màn hình
    • Chạy với :after (move-characters) để tránh vấn đề khởi tạo NaN sau system di chuyển

Tải va chạm và object bằng thuộc tính Tiled

  • Ban đầu, tường chỉ là ảnh thông thường giống tile sàn nên người chơi đi xuyên qua tường
  • Tạo lớp map-tile bằng custom type của Tiled và thêm member Boolean obstacle
    • Thêm thuộc tính map-tile vào tile tường và đánh dấu obstacle
  • Hàm properties->spec chuyển hash table thuộc tính của Tiled thành đặc tả object ECS
    • Custom class của Tiled được xử lý như component
    • Member của class được xử lý như slot của component
    • Ví dụ có dạng ((:map-tile :obstacle t))
  • load-tile-prefab đưa kết quả properties->spec vào đặc tả prefab
    • Nếu không có thuộc tính, spec-adjoin sẽ thêm component map-tile mặc định và obstacle nhận giá trị mặc định nil
  • Thêm slot tile-hash và chỉ mục tiles vào component position
    • tile-hash ép x, y thành số nguyên rồi đóng gói thành một số nguyên 64-bit duy nhất
    • Có thể dùng chỉ mục tiles để tìm mọi entity nằm tại tọa độ góc trên bên trái của một tile cụ thể
  • tile-start trả về tọa độ góc trên bên trái của tile lưới chứa một tọa độ bất kỳ
  • tile-obstacle-p kiểm tra xem trong các entity ở cùng tọa độ có tile nào là map-tile và có obstacle là true hay không
  • obstaclep kiểm tra xem tile tại một tọa độ bất kỳ có phải vật cản hay không
  • control-player kiểm tra các tile ở những góc liên quan của hình chữ nhật nhân vật theo hướng di chuyển, và nếu gặp vật cản thì trả tọa độ đích về vị trí hiện tại
  • Cách xử lý va chạm này chưa hoàn hảo
    • Nếu thiết kế dựa trên tọa độ tâm nhân vật thì toán học và mã có thể đơn giản hơn, nhưng ví dụ giữ nguyên cách hiện tại để tránh tăng độ phức tạp

Tải người chơi và nhân vật hoạt ảnh từ bản đồ

  • Thêm các custom class characterplayer trong Tiled
    • character chỉ có member float speed
    • target-x, target-y được bỏ qua để dùng giá trị mặc định
    • player có member int player với giá trị mặc định là 1
  • Đặt nhân vật người chơi trong object layer của Tiled dưới dạng tile object, rồi gán thuộc tính characterplayer
  • load-map được mở rộng để xử lý cả tiled:object-layer
    • Thuộc tính object được chuyển thành component ECS bằng properties->spec
    • Với tiled:tile-object, load-tile sẽ sao chép dữ liệu tile và hoạt ảnh rồi thiết lập vị trí
    • Tọa độ object của Tiled lấy góc dưới bên trái làm chuẩn, nên cần trừ chiều cao object khỏi y để khớp với chuẩn góc trên bên trái
  • Lời gọi load-player hardcode và chính hàm này đều được gỡ bỏ
  • Cấu trúc này đọc trực tiếp dữ liệu bản đồ Tiled thành object ECS, gần với lập trình hướng dữ liệu hơn
  • Hoạt ảnh nhân vật dùng các sequence orc-idleorc-run của orc được định nghĩa trong tileset
  • change-animation-sequence đổi hoạt ảnh hiện tại của entity
    • Nếu đã là cùng sequence thì không làm gì cả
    • Tìm frame đầu tiên của sequence mới bằng chỉ mục sequence-frames, rồi cập nhật animation-stateimage-bitmap
  • move-characters chuyển sang :orc-idle khi nhân vật đứng yên, và :orc-run khi đang di chuyển

Kẻ địch, game over, tìm đường A*

  • Component enemy có hai slot cần cho hành vi của kẻ địch
    • vision-range: khoảng cách bắt đầu nhìn thấy và phản ứng với người chơi
    • attack-range: phạm vi tấn công
  • Để kết thúc game, thêm biến toàn cục *should-quit*, và vòng lặp chính sẽ thoát nếu giá trị này là true
  • Hệ thống handle-enemies lấy tọa độ người chơi và so sánh với kẻ địch
    • Nếu người chơi ở trong tầm nhìn, đặt tọa độ mục tiêu của kẻ địch thành vị trí người chơi
    • Nếu người chơi ở trong tầm tấn công, đặt *should-quit* thành true và hiển thị hộp thoại thông báo native You died
  • Hoạt ảnh của kẻ địch dùng các sequence demon-idle, demon-run
    • move-characters chọn hoạt ảnh orc cho người chơi và hoạt ảnh quỷ cho kẻ địch tùy theo kết quả has-player-p
  • Với cách đuổi trực tiếp, kẻ địch cũng đi xuyên tường nên thêm tìm đường A*
  • Thêm cl-astar vào dependencies
    • Thư viện này tạo các hàm tìm đường được tối ưu cho bài toán bằng macro
  • Thay vì lưu đường đi trong mảng trong slot của component, mỗi điểm trên đường được biểu diễn bằng một entity riêng
    • path-pointx, y, traveller, và traveller có chỉ mục path-points
    • path lưu đích cuối cùng destination-x, destination-y
    • Tọa độ mục tiêu của character là điểm đường đi kế tiếp, còn path biểu diễn đích cuối cùng
  • Hệ thống follow-path lấy điểm đường đi đầu tiên và di chuyển nhân vật đến điểm đó
    • Khi đến điểm, xóa entity path-point tương ứng
    • Nếu không còn điểm nào nữa thì xóa component path
  • find-path được định nghĩa bằng a*:define-path-finder
    • Kích thước world được tính bằng cách chia kích thước cửa sổ cho kích thước tile
    • Dùng bộ đánh chỉ mục row-major
    • Việc đã đến đích hay chưa được xác định bằng việc tọa độ tile có trùng nhau hay không
    • Các láng giềng được liệt kê theo 8 hướng
    • Vật cản hoặc di chuyển chéo cắt qua vật cản được gán chi phí most-positive-single-float để khiến nó gần như bất khả thi
    • Heuristic dùng octile distance
    • Nếu đã có đường đi cũ thì xóa các điểm đường đi và gán path mới
    • Mỗi điểm trong đường đi kết quả được tạo thành một entity có path-pointparent
  • handle-enemies sẽ gọi find-path khi kẻ địch nhìn thấy người chơi mà đích của đường đi hiện tại khác vị trí người chơi
  • Sau thay đổi này, kẻ địch sẽ đuổi theo người chơi nhưng tránh vật cản khi di chuyển

UI game dựa trên Nuklear

  • Cần GUI cho các yếu tố tường thuật, nhưng các thư viện GUI truyền thống như Qt hay GTK không phù hợp với UI game được vẽ trong graphics context của liballegro
  • Dùng Nuklear làm thư viện UI
    • Có binding Common Lisp cl-liballegro-nuklear để dùng cùng liballegro
    • Binding này cũng cung cấp DSL cho giao diện khai báo
  • Thêm dependency cl-liballegro-nuklear/declarative và thêm file mới src/narrative.lisp
  • Trong package, đăng ký local nickname ui để tham chiếu ngắn gọn đến cl-liballegro-nuklear/declarative
  • Dùng Alegreya từ Google Fonts làm font UI, và đổi tên file thành alegreya-sc.ttf
  • ui:defwindow narrative định nghĩa hàm cửa sổ tường thuật
    • Vị trí cửa sổ được tính trong vùng trung tâm màn hình
    • Hiển thị văn bản tự xuống dòng bằng ui:label-wrap
    • ui:button-label "Ok" trả về true khi được nhấn
  • Nuklear là thư viện UI immediate mode
    • Không giữ các object widget trong bộ nhớ như retained mode, mà render và xử lý ở mỗi frame
    • Việc nhấn nút được xử lý bằng giá trị trả về và điều kiện ở mỗi frame chứ không phải callback
  • main.lisp tải font UI và khởi tạo UI context bằng nk:allegro-init
    • Trong event loop, gọi nk:input-begin, nk:allegro-handle-event, nk:input-end
    • Khi render, gọi nk:allegro-render
    • Khi thoát, gọi nk:allegro-shutdown, nk:allegro-font-del

Skin UI và object tường thuật

  • UI mặc định khá đơn điệu nên dùng asset ảnh fantasy-ui-borders của Kenney để tạo kiểu
  • Lưu các ảnh UI vào các biến toàn cục *window-background*, *button-normal-background*, *button-hover-background*, *button-active-background*
  • load-ui tải ảnh bằng nk:allegro-create-image, còn unload-ui giải phóng resource ảnh phía C bằng nk:allegro-del-image
  • Gọi load-ui trong init, và gọi unload-ui khi vòng lặp chính kết thúc
  • Đặt ảnh nền, ảnh theo trạng thái nút và màu chữ bằng đối số :styles của ui:defwindow
  • Component narrative biểu diễn các object dùng cho environmental storytelling
    • text: văn bản sẽ hiển thị
    • shown: đã từng được hiển thị ít nhất một lần hay chưa
    • active: cửa sổ hiện có đang được kích hoạt hay không
    • active có chỉ mục active-narratives
  • Hệ thống show-narrative hiển thị cửa sổ khi người chơi ở gần object tường thuật
    • Khoảng cách tương tác được tính từ +interact-distance-factor+ và kích thước tile của người chơi
    • Hiển thị nếu cửa sổ đã đang active, hoặc chưa từng được hiển thị, hoặc phím E được nhấn
    • Có thể đóng cửa sổ bằng nút Ok, Esc, Space hoặc Enter
  • Trong Tiled, tạo custom type narrative và thêm member string text
  • Để phát hiện va chạm và căn khớp chính xác, các object không thể đi xuyên phải được căn tọa độ theo lưới tile
  • Vấn đề dungeon vẫn tiếp tục chuyển động khi cửa sổ tường thuật mở được ngăn bằng điều kiện chạy hệ thống
    • Thêm :when (null (active-narratives t)) vào move-characterscontrol-player
    • Nếu có narrative đang active thì các hệ thống di chuyển và điều khiển sẽ không chạy
  • Điều kiện chiến thắng được thêm bằng tag component win
    • Nếu đóng cửa sổ trên object có cả narrative, đặt *should-quit* thành true để kết thúc game

Kết lại và phạm vi

  • Ví dụ hoàn chỉnh dùng cl-fast-ecs, cl-tiled, cl-astar, cl-liballegro-nuklear để tạo một dungeon crawler kiểu Souls-like có environmental storytelling, AI kẻ địch và GUI
  • Quy mô triển khai khoảng 500 dòng code
  • Toàn bộ code có trong kho GitHub, và ngoài code tutorial còn có thêm các khai báo kiểu tùy chọn dùng declaim
  • Không đề cập đến sound design, cutscene, menu chính, chuyển level, hay “door problem”
  • Autumn Lisp Game Jam 2024 sẽ diễn ra trên itch.io vào ngày 25 tháng 10 năm 2024, là sự kiện làm game trong 10 ngày bằng các phương ngữ Lisp rồi đánh giá và phản hồi lẫn nhau
  • Phần này dựa trên tác phẩm dự thi Spring Lisp Game Jam 2023 Thoughtbound
  • Ở phần tiếp theo, tác giả báo trước sẽ mở rộng quy mô và thêm AI nâng cao hơn để thử thách làm một game chiến thuật thời gian thực

1 bình luận

 
GN⁺ 2024-10-19
Ý kiến trên Hacker News
  • Ước gì mọi tutorial kỹ thuật đều được viết như thế này. Bố cục bài viết rất tốt, hầu như không có lỗi ngữ pháp, và mỗi khi giới thiệu một chủ đề mới thì đều giải thích với dung lượng vừa đủ, lại còn có ví dụ mã hoàn chỉnhtư liệu trực quan cho thấy đoạn mã thực sự làm gì
    Bài đủ dài để đi sâu vào tài liệu, nhưng cũng đủ độc lập để vẫn theo kịp ngay cả khi chưa đọc phần 1 và chỉ mới đụng Common Lisp vài tháng từ mấy năm trước. Tôi cũng từng dùng Clojure và Emacs Lisp khá nhiều
    Bravo, awkravchuk/Andrew :^)
    (Đăng chéo từ https://mxjn.me/2024/10/17/1)

    • Trên hết, điểm lớn nhất là đây là bài viết chứ không phải video. Có thể sao chép và dán, đọc rõ ràng, theo dõi theo tốc độ của riêng mình, và tiếp nhận trong yên tĩnh
      Cũng có lợi thế là dễ lưu lại để dùng offline hoặc lưu trữ, có thể ghi chú, và tìm kiếm cũng dễ
  • Hiếm có điều gì trong giới kỹ thuật làm tôi xúc động như một dự án hay bài viết xuất sắc về Common Lisp. Đây thực sự là một món quà lớn
    Tôi đã đọc phần 1 khi nó ra mắt, và thực sự rất mong được đọc phần này nữa. Xin gửi lời khen ngợi đến tác giả

  • Chỉ riêng package.sh và toàn bộ phần quản lý build cho 3 hệ điều hành cũng đã là một lớp học bậc thầy rồi. Chỉ cần lướt qua kho GitHub thôi tôi cũng đã học được rất nhiều
    Thông thường tôi build ứng dụng dòng lệnh Common Lisp bằng SBCL hoặc LispWorks, nhưng lần tới có lẽ tôi sẽ thử với ECL. Việc có cả build cho macOS lẫn Linux thật tuyệt, và cũng có vẻ thú vị khi thử điều gì đó mới

    • Tôi đã xây dựng cấu hình CI đó trên hạ tầng CL suốt mấy năm nay, và nó vẫn cứ bị hỏng :D
  • Bài viết rất hay. Tôi đang phát triển một game bắn phép thuật góc nhìn người thứ ba nhiều người chơi, bằng Lisp, chính xác hơn là ClojureScript. Đây là một game 3D trên nền web, và tôi định sẽ viết blog về hành trình này, bao gồm cả các công cụ và lớp trừu tượng tôi đã tạo ra cho dự án
    Nếu quan tâm thì bản demo ở đây: https://wizardmasters.io

    • Jon Blow cũng từng cố làm một game kiểu này từ lâu rồi. Xem thử ông ấy đã thất bại như thế nào và vì sao có thể sẽ học được điều gì đó
  • Bản thân bài viết thì thực sự rất chắc tay, nhưng khi nhìn vào quy trình thiết lập ở phần 1, trải dài qua chính Common Lisp, Python, C và nhiều bước khác nhau, thì cũng phần nào cho thấy vì sao CL lại không quá phổ biến, đặc biệt với các lập trình viên trẻ
    Thật đáng tiếc, và sẽ rất tốt nếu có ai đó chịu khó làm cho ngôn ngữ dễ tiếp cận hơn ở khía cạnh cài đặt

    • Không chắc là nhắm đúng chính xác cùng một vấn đề, nhưng https://ciel-lang.org/ ít nhất cũng là một nỗ lực để giải quyết một phần vấn đề quá nhiều bước
      Theo tôi hiểu thì nó tập trung nhiều hơn vào chuyện có quá nhiều lựa chọn và các thiết lập mặc định lâu đời trông đã lỗi thời
  • Vòng lặp sự kiện là một ví dụ tuyệt vời cho thấy loop là một ngôn ngữ chuyên biệt cho lặp nghiêm túc đến mức nào. Thích hay ghét thì cũng vậy ;)

    • Sao không dùng https://iterate.common-lisp.dev/ thay cho loop? Không có cú pháp lạ không phải S-expression, cũng không cần do để quay về cú pháp Lisp
      Có thể dùng if/when thông thường mà không có else/end trông khó chịu, và nhìn chung còn bổ sung thêm nhiều tính năng hữu ích
    • Lúc đầu tôi còn cười nhạo nó, nhưng sau vài năm lập trình với Common Lisp, loop đã trở thành một trong những thành phần CL tôi thích nhất
  • Bài này làm tôi nhớ đến "Caves of Clojure": https://stevelosh.com/blog/2012/07/caves-of-clojure-01/

  • Đúng lúc tuần này tôi vừa bắt đầu làm một roguelike bằng Python, nên thử làm bằng Lisp nghe cũng hay đấy

  • Cảm giác như bị lừa vậy. Tôi đến để học cách làm một game đơn giản, rồi lại học được quá nhiều thứ về toàn bộ lĩnh vực điện toán
    Thật sự rất tuyệt