Lúc bắt đầu làm phần này, mình cứ nghĩ "AI quái vật" là chuyện to tát: phải có tìm đường, có bản đồ, có cây quyết định. Làm xong rồi nhìn lại thì toàn bộ não con zombie nằm gọn trong một câu switch với 8 nhánh, và không có lấy một hàm tìm đường nào.
Nhưng đúng là mấy tuần ngồi với nó mình mới hiểu: cái khó không nằm ở chỗ làm nó khôn. Cái khó là làm nó ngu đúng kiểu, ngu một cách đáng tin.
Chương 18 cái hộp, không hơn
Mỗi khung hình, con zombie chỉ làm đúng một việc: xem mình đang ở hộp nào, rồi kiểm tra vài điều kiện xem có nên nhảy sang hộp khác không.
| Trạng thái | Nó đang làm gì |
|---|---|
idle | Đứng im, đếm ngược 0.5–1.5 giây |
patrol | Đi tới trước với tốc độ 0.47 px mỗi khung |
turn | Chạy animation quay đầu, xong thì đổi hướng |
investigate | Đi về phía toạ độ vừa nghi ngờ, tốc độ 1.0 |
chase | Đuổi với tốc độ 0.78 |
attack | Cào tại chỗ, 10 máu |
lunge | Lao tới húc, 22 máu |
hurt | Choáng 0.4 giây rồi quay lại đuổi |
Thật ra ba hộp đầu chiếm gần hết thời gian sống của nó. Một con zombie bình thường cả đời chỉ đi qua đi lại giữa idle → patrol → turn, cho tới lúc có ai đó bước vào tầm.
Lúc đi tuần, mỗi khung hình nó dò hai thứ ngay trước mũi chân: phía trước còn sàn không, và phía trước có tường không. Thiếu sàn hoặc gặp tường là chuyển sang turn. Chỉ vậy thôi mà đủ để nó không bao giờ tự đi rơi xuống hố trong lúc đi tuần.
Nhưng lúc đuổi thì mình cố ý bỏ hẳn cái kiểm tra đó đi. Đang đuổi mà tới mép sàn thì nó cứ thế bước ra và rơi xuống. Zombie mà biết dừng lại ở mép vực thì còn gì là zombie nữa.
Chương 2Ba giác quan, mỗi cái bịt một lỗ hổng
Đây là phần mình sửa đi sửa lại nhiều nhất, vì mỗi lần thêm một giác quan là lại lòi ra một kiểu "ăn gian" mới của người chơi.
Mắt — 130px, chỉ phía trước mặt. Nói cho sang là "nón nhìn", chứ thật ra trong code nó chỉ là một phép so sánh trái/phải: nhân vật có nằm về phía nó đang quay mặt không. Không có góc dọc, không có tia chiếu. Đứng ngay sau lưng nó thì dù cách 10px nó cũng mù tịt.
Tầm nhìn này còn bị nhân lên nhân xuống:
- Mình ngồi thấp → nhân 0.35, tụt từ 130px xuống còn 45px.
- Mình bật đèn pin → nhân 1.6, nở ra thành 208px.
Tai — 110px, cả hai phía. Thêm cái này vì chỉ có mắt thì đi vòng ra sau lưng là qua mặt được mọi lúc, dễ đến mức nhàm. Nhưng tai có hai điều kiện ngặt: chỉ nghe khi mình đang di chuyển và không ngồi thấp. Đứng im thì nó điếc hoàn toàn.
Chạm — 36px, mọi hướng. Cái này là để bịt lỗ cuối. Không có nó thì người chơi có thể ngồi im đè lên người con zombie mà nó vẫn thản nhiên đi tuần — nhìn vô lý ngay lập tức. Trong 36px là lộ, bất kể nó quay mặt đâu, bất kể mình ngồi hay đứng.
Ba cái này ghép lại mới ra được cái cảm giác mình muốn: lén được, nhưng phải lén có kỹ thuật. Ngồi thấp, đi sau lưng, tắt đèn, và đừng lại gần quá.
Chương 3Cả đàn kéo tới — mà không con nào nói chuyện với con nào
Lúc đầu mỗi con zombie tự lo thân nó. Chơi vào thấy rời rạc: mình bắn một phát giữa hành lang, con ngay cạnh lao tới còn con cách đó vài bước vẫn đi tuần như không có gì.
Mình thêm cơ chế báo động. Nhưng điểm mình thích nhất là nó không hề thông minh như nghe tả.
Có ba chuyện kích hoạt báo động:
- Mình bắn súng → đánh thức mọi zombie trong 420px.
- Một con bị đánh trúng → nó đánh thức đồng bọn trong 260px.
- Một con vừa phát hiện ra mình → cũng đánh thức đồng bọn trong 260px.
Và đây mới là chỗ đáng nói: zombie không hề biết mình đang ở đâu. Cái duy nhất được truyền đi là một cặp toạ độ — chỗ vừa phát ra tiếng động. Mỗi con được dúi cho toạ độ đó, chuyển sang investigate, chạy tới đúng chỗ ấy rồi dò quanh 2.5 giây.
Nghĩa là bắn xong mà dời chỗ ngay thì cả đám kéo tới một chỗ trống. Mình không hề thiết kế chuyện đó, nó tự nhiên rơi ra từ cơ chế. Nhưng chơi vào thì lại đúng là mẹo chiến thuật hay nhất của khẩu súng: tiếng súng vừa là vũ khí, vừa là mồi nhử.
Chương 4Phần khó nhất: dạy nó biết bỏ cuộc
Làm zombie biết đuổi thì dễ. Làm nó biết thôi đuổi mới mệt, và đây là chỗ mình sửa nhiều nhất.
Van 1 — thấy ở 130px, nhưng mất dấu ở 190px. Hai con số lệch nhau 60px là cố ý. Hồi đầu mình để chung một mốc, kết quả là đứng đúng ngay ranh giới thì con zombie bật/tắt trạng thái mỗi khung hình, chạy tới lui tại chỗ như bị giật. Để ngưỡng buông xa hơn ngưỡng thấy là hết.
Van 2 — khác tầng quá 1.5 giây thì thôi. Vì không có tìm đường, nên khi mình leo lên nóc thùng hàng, con zombie dưới đất chỉ biết đi ngang qua lại dưới chân, vĩnh viễn. Giờ nó đếm: cứ mỗi khung hình thấy chênh cao quá 40px thì cộng một; đủ 90 khung (1.5 giây) là bỏ, quay về đi tuần. Nhìn ngoài thì giống như nó nản.
Van 3 — dò 2.5 giây không ra thì lờ đi 2.5 giây. Cái này là quan trọng nhất, và cũng là lỗi làm mình mất nhiều thời gian nhất. Dò tìm xong mà không thấy gì, con zombie bật một lá cờ "lờ đi" trong 150 khung hình — trong lúc đó dù có phát hiện ra mình nó cũng mặc kệ.
Không có van này thì chuyện xảy ra thế này: nó dò tìm xong, chuyển về idle, khung hình ngay sau đó lại phát hiện ra mình, lại chuyển sang chase, lại mất dấu, lại dò tìm — kẹt vòng lặp vĩnh viễn ngay tại chỗ, co giật liên tục. Phải có một khoảng thời gian nó cố tình giả vờ không thấy thì mọi thứ mới mượt.
Cả ba đều chỉ là bộ đếm. Không có trí thông minh nào ở đây cả. Nhưng chơi vào thì giống hệt một con vừa chán, vừa ngu, vừa dễ bị đánh lừa — đúng thứ mình cần.
Chương 5Cú lao tới: tung xúc xắc đúng một lần
Nếu zombie chỉ biết đi tới rồi cào thì đánh nhau nhạt lắm. Mình thêm đòn lao tới: ở khoảng cách 90–220px, nó có thể bất ngờ phóng tới với tốc độ 9 px mỗi khung — nhanh gấp hơn 11 lần tốc độ đuổi bình thường.
Chi tiết mình thích nhất ở đòn này là cách tung xúc xắc.
Lần đầu mình viết kiểu "mỗi khung hình trong tầm thì có 40% cơ hội lao". Nghe thì hợp lý, nhưng thực tế là đứng trong tầm nửa giây thôi đã quay 30 lần xúc xắc — nghĩa là gần như chắc chắn lao ngay lập tức, lần nào cũng vậy. Đòn bất ngờ thành đòn mặc định.
Sửa lại: mỗi lần bước vào vùng 90–220px thì chỉ được quay đúng một lần. Quay trượt là thôi, phải ra khỏi vùng rồi vào lại mới được quay lần nữa. Giờ thì 40% đúng nghĩa là 40%: có lúc nó lao, có lúc nó chỉ lầm lũi đi tới, và mình không đoán được lần nào ra lần nào.
Trúng cú lao là 22 máu — hơn gấp đôi cú cào thường — kèm khựng cả màn hình 5 khung hình cho đủ nặng.
Chương 6Mấy chỗ phải ăn gian
Viết xong rồi mới thấy phần lớn công sức nằm ở mấy chỗ chắp vá, không phải ở "AI".
Cú lao phải kiểm tra bằng khoảng cách, không phải va chạm. Lao với tốc độ 9 px mỗi khung thì nó xuyên thẳng qua người nhân vật giữa hai khung hình, và phép kiểm tra va chạm không bao giờ bắt được. Phải đổi sang đo khoảng cách: cứ vào trong 30px là tính trúng.
Lúc trúng thì đẩy hẳn vào trong người. Nếu để nó dừng đúng ở mép, nhìn như hai khối hộp chạm nhau. Giờ lúc trúng mình dời nó vào sâu thêm 8px, đè hẳn lên người nhân vật — trông mới ra cú húc.
Đòn cào ăn ở 60% animation. Không phải lúc bắt đầu vung, cũng không phải lúc kết thúc. Chọn đúng khung mà móng vuốt chạm tới thì cảm giác mới khớp.
Leo bậc bằng cách dịch lên 32px. Gặp vật cản thì nó thử nhấc mình lên 32 pixel xem có lọt không — lọt thì đi tiếp. Đây đúng nghĩa là ăn gian, nhưng rẻ hơn tìm đường rất nhiều và nhìn ngoài không ai nhận ra. Nếu không lọt thì nó vấp: dừng lại, văng ít bụi, và mình khoá cái hiệu ứng vấp đó lại 45 khung hình để nó khỏi rung liên tục tại chỗ.
Hộp va chạm cố định 21×48. Không dùng khung bao của sprite, vì khung bao thay đổi theo từng animation — lúc nó vung tay thì khung phình ra, đạn bắn vào khoảng không cạnh người vẫn tính trúng. Hộp cố định thì bắn vào đâu trúng đó.
Chương 7Nhìn lại
| Lỗi gặp phải | Cách sửa |
|---|---|
| Đứng ranh giới làm zombie giật qua lại | Tách ngưỡng thấy (130) và ngưỡng buông (190) |
| Zombie kẹt dưới chân khi mình đứng trên cao | Đếm 1.5 giây khác tầng thì bỏ cuộc |
| Dò tìm xong lại phát hiện lại, kẹt vòng lặp | Bật cờ "lờ đi" 2.5 giây sau khi dò hụt |
| Đòn lao tới lần nào cũng xảy ra | Chỉ quay xúc xắc một lần mỗi khi vào tầm |
| Lao nhanh quá nên xuyên qua người | Đo khoảng cách thay vì chờ va chạm |
| Đi vòng ra sau lưng là qua mặt được | Thêm tai nghe tiếng bước, 110px |
| Ngồi im đè lên zombie mà nó không biết | Thêm tầm chạm 36px, mọi hướng |
Mấy điều mình rút ra:
- Kẻ địch không cần thông minh, cần dễ đoán theo kiểu thú vị. Người chơi phải học được luật của nó, rồi lợi dụng luật đó.
- Mỗi giác quan phải bịt lỗ hổng của giác quan trước. Thêm một cái mới thì phải đi tìm xem nó mở ra lỗ hổng gì.
- Biết bỏ cuộc quan trọng ngang biết đuổi. Quá nửa số bug ở phần này là vì nó không chịu buông.
- Bộ đếm rẻ hơn thuật toán. Không có tìm đường mà vẫn đủ dùng, chỉ cần nó biết nản đúng lúc.
Chương 8Tiếp theo
Con zombie thì xong rồi. Cái mình muốn làm kế tiếp là bandit — đám cướp còn sống, và đây sẽ là một bài toán khác hẳn: chúng biết ngắm và bắn từ xa, biết tìm chỗ nấp, biết lùi lại khi sắp chết, và có lẽ phải biết gọi đồng bọn đúng nghĩa chứ không chỉ dúi nhau một toạ độ.
Nói cách khác: zombie đáng sợ vì nó không nghĩ. Bandit sẽ phải đáng sợ vì nó có nghĩ. Chắc sẽ là bài devlog dài hơn bài này nhiều.