Tiêu chuẩn vàng về tối ưu hóa: Cái nhìn sâu sắc về RollerCoaster Tycoon
Backend·Hacker News·2 lượt xem

Tiêu chuẩn vàng về tối ưu hóa: Cái nhìn sâu sắc về RollerCoaster Tycoon

The gold standard of optimization: A look under the hood of RollerCoaster Tycoon

AI Summary

Bài viết nói về cách game RollerCoaster Tycoon (phát hành năm 1999) được tối ưu hóa đến mức đáng kinh ngạc. Bí quyết chính nằm ở việc Chris Sawyer, người tạo ra game, đã viết phần lớn code bằng Assembly. Dù cách làm này khá hiếm ngay cả thời điểm đó, nhưng nó cho phép game chạy mượt mà trên phần cứng của năm 1999, xử lý được những công viên phức tạp với hàng ngàn "agent" (nhân vật, khách tham quan) một cách hoàn hảo. Các developer có thể rút ra bài học từ cách Sawyer tối ưu hóa cẩn thận ở mọi khía cạnh. Ví dụ, ông sử dụng các kiểu dữ liệu có kích thước biến đổi (variable-sized data types) cho các giá trị tài chính, hoặc dùng các phép toán trên bit (bitwise operations) thay vì các hàm toán học thông thường ở những chỗ có thể. Điều này cho thấy việc hiểu sâu về phần cứng và thực hiện các kỹ thuật "micro-optimization" quyết liệt có thể mang lại những cải thiện hiệu năng đáng kể.

Do một số trường hợp may mắn, gần đây tôi có cơ hội xuất hiện trên một trong những podcast về trò chơi lớn nhất của Đức, Stay Forever, để nói về công nghệ của RollerCoaster Tycoon (1999).

Do một số trường hợp may mắn, gần đây tôi đã có cơ hội xuất hiện trên một trong những podcast về trò chơi lớn nhất của Đức, Stay Forever, để nói về công nghệ của RollerCoaster Tycoon (1999). Đó là một cuộc phỏng vấn tuyệt vời và tôi thực sự khuyên bạn nên nghe toàn bộ tập tại đây, ít nhất là nếu bạn nói tiếng Đức. Nếu không, đừng lo lắng—bài viết này đề cập đến những gì đã được nói (và một chút nữa).

RollerCoaster Tycoon và phần tiếp theo của nó thường được coi là một trong những trò chơi được tối ưu hóa tốt nhất hiện có, được viết gần như hoàn toàn bằng Assembly bởi người tạo ra chúng, Chris Sawyer. Bằng cách nào đó, trò chơi này đã mô phỏng được đầy đủ các công viên chủ đề với hàng nghìn tác nhân trên phần cứng của năm 1999 mà không phải đổ một giọt mồ hôi nào. Một thành tích vô cùng ấn tượng khi xét đến thực tế là ngày nay, rất nhiều trò chơi xây dựng tương tự vẫn phải vật lộn để đạt được tốc độ khung hình ổn định.

Vậy Chris Sawyer đã làm cách nào để đạt được điều này?

Có rất nhiều câu trả lời cho câu hỏi này, một số câu hỏi nhỏ và tập trung, một số câu trả lời rộng rãi và có tác động mạnh mẽ. Điều được đề cập đầu tiên trong hầu hết các bài viết là trò chơi được viết bằng ngôn ngữ cấp thấp Assembly, đặc biệt là vào thời điểm phát triển trò chơi, cho phép anh viết các chương trình hiệu quả hơn so với khi anh sử dụng các ngôn ngữ cấp cao khác như C hoặc C++.

Viết mã trong hội đã là tiêu chuẩn để phát triển trò chơi trong một thời gian dài nhưng tại thời điểm này về cơ bản nó đã là một thói quen đã từ bỏ. Ngay cả Doom đầu tiên, được phát hành sáu năm trước, hầu hết đã được viết bằng C và chỉ một số phần được viết bằng Assembly, và không ai có thể tranh cãi rằng Doom theo bất kỳ cách nào là một trò chơi chưa được tối ưu hóa.

Thật khó để kiểm tra chắc chắn nhưng có khả năng RCT là trò chơi lớn cuối cùng được phát triển theo cách này. Tác động đến hiệu suất vào thời điểm đó lớn đến mức nào thì khó có thể định lượng được, nhưng xét về giá trị của nó thì có lẽ nó đã cao hơn hiện nay. Trình biên dịch đã trở nên tốt hơn nhiều trong việc tối ưu hóa mã cấp cao và nhiều hoạt động tối ưu hóa mà bạn cần thực hiện thủ công trước đây có thể được xử lý bởi trình biên dịch ngày nay.

Nhưng bên cạnh việc sử dụng lắp ráp, mã RCT đã được tối ưu hóa mạnh mẽ. Làm sao chúng tôi biết được điều này nếu mã nguồn chưa bao giờ được phát hành? Chúng tôi có một thứ gần như tốt như vậy: Triển khai lại nó tương thích 100%, OpenRCT2.

Được viết bởi (rất) những người hâm mộ tận tâm, OpenRCT2 quản lý để triển khai lại toàn bộ RollerCoaster 1&2 bằng cách sử dụng nội dung ban đầu. Mặc dù đây KHÔNG phải là mã nguồn ban đầu, đặc biệt là trong các phiên bản trước đó, việc triển khai lại này rất giống với mã gốc, dựa trên nhiều năm kỹ thuật đảo ngược. Lưu ý rằng hiện tại, OpenRCT2 ngày càng có nhiều cải tiến hơn so với mã gốc. Tôi sẽ lưu ý một số thay đổi đó khi chúng tôi gặp chúng.

Ngoài ra, tôi sẽ không thực hiện tất cả các hoạt động tối ưu hóa mà sẽ chọn một số ví dụ chỉ để minh họa rằng mọi phần của trò chơi đều đã được tối ưu hóa đến mức gần kề.

Các loại tiền

Bạn sẽ lưu trữ giá trị tiền trong trò chơi bằng cách nào? Bạn có thể sẽ bắt đầu bằng cách nghĩ về giá trị tiền cao nhất có thể mà bạn có thể cần trong trò chơi và chọn loại dữ liệu dựa trên đó. Chris Sawyer rõ ràng đã làm điều tương tự, nhưng theo cách chi tiết hơn.

Các giá trị tiền khác nhau trong mã sử dụng các loại dữ liệu khác nhau, dựa trên giá trị mong đợi cao nhất tại thời điểm đó. Ví dụ: biến lưu trữ giá trị công viên tổng thể sử dụng 4 byte vì giá trị công viên tổng thể dự kiến ​​sẽ sử dụng số khá cao. Nhưng giá có thể điều chỉnh của một mặt hàng cửa hàng? Điều này yêu cầu phạm vi số thấp hơn nhiều, vì vậy trò chơi chỉ sử dụng một byte để lưu trữ nó. Lưu ý rằng đây là một trong những tính năng tối ưu hóa đã bị loại bỏ trong OpenRCT2, tính năng này đã thay đổi tất cả các lần xuất hiện thành một biến 8 byte đơn giản, vì trên các CPU hiện đại, tính năng này không còn tạo ra sự khác biệt về hiệu năng nữa.

Thay thế các phép toán bằng dịch chuyển bit

Khi đọc qua nguồn của OpenRCT2, có một cú pháp phổ biến mà bạn hiếm thấy trong mã hiện đại, những dòng như thế này:

Nhờ nạp chồng toán tử, toán tử '<<' có thể có nhiều ý nghĩa trong C++. Những gì dòng này thực hiện một cách hiệu quả cũng giống như những gì hầu hết các lập trình viên sẽ viết như thế này:

Việc toán tử '<<' thực hiện ở đây được gọi là dịch chuyển bit, nghĩa là tất cả các bit lưu trữ giá trị của biến được dịch chuyển sang trái, trong trường hợp này là hai vị trí, với các chữ số mới được điền bằng số 0. Vì số được lưu dưới dạng hệ nhị phân nên mỗi lần dịch sang trái nghĩa là số đó được nhân đôi.

Lúc đầu, điều này nghe có vẻ khó hiểu về mặt kỹ thuật, nhưng khi nhân các số trong hệ thập phân, về cơ bản chúng ta cũng làm như vậy. Khi bạn nhân 57 * 10, bạn có thực sự 'tính' phép nhân không? Hay bạn chỉ thêm số 0 vào số 57? Đó là nguyên tắc tương tự chỉ với một hệ thống số khác.

Thủ thuật tương tự cũng có thể được sử dụng cho hướng khác để cứu một bộ phận:

Điều này về cơ bản giống với 

RCT luôn thực hiện thủ thuật này và ngay cả trong phiên bản OpenRCT2, cú pháp này vẫn không bị thay đổi vì trình biên dịch sẽ không thực hiện việc tối ưu hóa này cho bạn. Điều này có vẻ giống như một cơ hội bị bỏ lỡ nhưng hợp lý vì việc tối ưu hóa này sẽ trả về các kết quả khác nhau cho các trường hợp tràn và thiếu (dù sao thì mã cũng nên tránh).

Tuy nhiên, điểm thú vị hơn nữa về những phép tính đó là tần suất mã có thể thực hiện việc này. Rõ ràng, việc dịch chuyển bit chỉ có thể được thực hiện đối với các phép nhân và phép chia có lũy thừa bằng 2, như 2, 4, 8, 16, v.v. Thực tế là nó được thực hiện thường chỉ ra rằng các công thức trong trò chơi được thiết kế đặc biệt để tuân theo những con số đó bất cứ khi nào có thể, điều mà trong hầu hết các quy trình phát triển hiện đại về cơ bản là không thể. Hãy tưởng tượng một lập trình viên hỏi nhà thiết kế trò chơi rằng liệu họ có thể thay đổi công thức của mình để sử dụng số 8 thay vì 9,5 vì đó là con số mà CPU thích tính toán hơn. Có một lập luận rất thuyết phục được đưa ra rằng một nhà thiết kế trò chơi không bao giờ phải lo lắng về các đặc tính hiệu suất thời gian chạy của số học nhị phân trong cuộc sống của họ, đó là số phận dành riêng cho các lập trình viên. May mắn thay, trong trường hợp RCT, người thiết kế trò chơi và người lập trình trò chơi là cùng một người, điều này cũng mang lại sự chuyển đổi tốt sang tối ưu hóa lớn thứ ba:

Thiết kế trò chơi mang lại hiệu suất

RCT chưa bao giờ là một dự án thuần túy của một người, mặc dù nó thường được mô tả là một dự án. Ví dụ: tất cả đồ họa của trò chơi và các tiện ích bổ sung của nó đều do Simon Foster tạo ra, trong khi âm thanh là trách nhiệm của Allister Brimble.

Nhưng có lẽ đúng khi gọi nó là Trò chơi của Chris Sawyer, người là lập trình viên chính và là người thiết kế trò chơi duy nhất cùng nhau.

Sự chồng chéo về vai trò này cho phép thực hiện một số hoạt động tối ưu hóa sâu sắc, bằng cách không chỉ thiết kế trò chơi dựa trên trải nghiệm trò chơi dự kiến mà còn dựa trên đặc điểm hiệu suất của các quyết định thiết kế đó.

Một ví dụ điển hình cho điều này là tính năng tìm đường được sử dụng trong trò chơi. Khi viết tài liệu thiết kế trò chơi cho trò chơi xây dựng công viên, rất dễ dàng để thiết kế một giải pháp trong đó trước tiên khách quyết định điểm tham quan nào họ muốn ghé thăm (dựa trên sở thích đi xe của từng khách), sau đó đi bộ đến điểm thu hút họ đã chọn.

Tuy nhiên, từ quan điểm công nghệ, thiết kế này về cơ bản là một trường hợp xấu nhất. Tìm đường là một nhiệm vụ tốn kém và việc chạy nó cho hàng nghìn tổng đài viên cùng lúc là một viễn cảnh khó khăn, ngay cả trên các máy móc hiện đại. 

Đó có thể là lý do tại sao hành vi của khách trong RCT lại có sự khác biệt cơ bản. Thay vì chọn một chuyến đi để tham quan và sau đó tìm đường đến đó, những vị khách trong RCT đi bộ quanh công viên, về cơ bản là bị mù, chờ đợi để vô tình vấp phải một chuyến đi thú vị. Họ đi theo con đường hiện tại, không hề nghĩ đến việc đi lại hay nhu cầu gì cả. Khi đến một ngã ba, họ sẽ chọn một hướng đi mới gần như ngẫu nhiên, chỉ sử dụng một bộ quy tắc bổ sung rất nhỏ để tránh ngõ cụt, v.v. 

Thực ra “thiếu sót” này rất dễ nhận ra trong game, khi đi theo một vị khách quanh công viên một lúc. Họ không cố tình đi đến bất cứ đâu, ngay cả khi phàn nàn về đói khát, họ cũng không nghĩ đến việc tìm kiếm quán ăn gần nhất mà chỉ đi tiếp cho đến khi ngẫu nhiên đi ngang qua một quán ăn.

Điều này không có nghĩa là RCT hoàn toàn không thực hiện bất kỳ hoạt động tìm đường nào; có những trường hợp sử dụng công cụ tìm đường truyền thống. Ví dụ: nếu một thợ cơ khí cần đến một chuyến xe bị hỏng hoặc một vị khách muốn đến lối ra công viên, thì những trường hợp đó vẫn yêu cầu việc tìm đường truyền thống và do đó tốn kém.

Nhưng ngay cả trong những trường hợp đó, RCT vẫn lắp đặt một số lưới an toàn để tránh hiện tượng tăng vọt khung hình. Quan trọng nhất, pathfinder có giới hạn tích hợp về khoảng cách nó được phép đi qua mạng đường dẫn đối với một yêu cầu đường dẫn riêng lẻ. Nếu không tìm thấy đường dẫn nào trước khi đạt đến giới hạn này, trình tìm đường được phép hủy tìm kiếm và trả về kết quả là lỗi. Với tư cách là người chơi, bạn thực sự có thể thấy những thất bại của người tìm đường trong thời gian thực bằng cách đọc suy nghĩ của khách:

Đúng vậy, mỗi khi khách của công viên phàn nàn về việc không tìm được lối ra, về cơ bản đây là Pathfinder nói với trò chơi rằng có thể có một con đường, nhưng vì mục đích hoạt động, nó sẽ không tiếp tục tìm kiếm nó.

Phần này đặc biệt hấp dẫn đối với tôi vì nó biến việc tối ưu hóa được thực hiện không cần thiết về mặt kỹ thuật thành một tính năng chơi trò chơi. Điều gì đó khó có thể xảy ra trong quá trình phát triển trò chơi “hiện đại”, nơi vai trò của người lập trình và nhà thiết kế trò chơi được tách biệt hoàn toàn. Trong trường hợp giới hạn tìm đường, thậm chí còn có nhiều hệ thống trò chơi được kết nối với nó hơn. Theo mặc định, trình tìm đường chỉ được phép đi qua mạng đường dẫn ở độ sâu tối đa 5 điểm nối, nhưng giới hạn này không được đặt ra cố định. Ví dụ, cơ học được coi là quan trọng hơn đối với lối chơi so với khách bình thường, đó là lý do tại sao họ được phép chạy máy tìm đường với giới hạn tìm kiếm là 8 nút giao.

Nhưng ngay cả một vị khách bình thường của công viên cũng được phép tìm đường lâu hơn, chẳng hạn như bằng cách mua bản đồ công viên được bán tại ki-ốt thông tin.

Khi tìm kiếm đường đi cho khách đã mua bản đồ, giới hạn số đường dẫn được tăng từ 5 lên 7, giúp khách tìm lối ra công viên dễ dàng hơn.

Thay đổi thiết kế của trò chơi để cải thiện hiệu suất có vẻ như là một bước đi căn bản, nhưng nếu thực hiện đúng, nó có thể mang lại những lợi ích mà không một biện pháp tối ưu hóa vi mô cẩn thận nào có thể đạt được.

Đông người không ùn tắc

Một ví dụ khác về điều này là cách RCT xử lý các công viên quá đông đúc. Những con đường tắc nghẽn là cảnh tượng thường thấy ở mọi công viên giải trí, và rõ ràng, trò chơi cũng phải giải quyết chúng bằng cách nào đó. Nhưng giải pháp rõ ràng, triển khai một số dạng hệ thống tránh hoặc va chạm tác nhân, sẽ tác động đến tốc độ khung hình giống như Kryptonite làm với Superman.

Một lần nữa, giải pháp chỉ là vượt qua thách thức kỹ thuật. Các vị khách trong RCT không va chạm với nhau và cũng không cố gắng tránh mặt nhau. Trong thực tế, thậm chí hàng nghìn trong số chúng có thể chiếm cùng một ô đường dẫn:

Tuy nhiên, điều này không có nghĩa là người chơi không cần phải tính đến những công viên quá đông đúc. Mặc dù khách không tương tác với những khách xung quanh nhưng họ vẫn theo dõi họ. Nếu có quá nhiều khách khác ở gần, điều này sẽ ảnh hưởng đến niềm vui của họ và gây ra khiếu nại cho người chơi. Kết quả dành cho người chơi cũng tương tự, vì họ vẫn cần lập kế hoạch bố trí để tránh những con đường quá đông đúc, nhưng các phép tính cần thiết cho việc triển khai này sẽ được xử lý nhanh hơn rất nhiều.

RCT có thể là “cơn bão hoàn hảo” cho phương pháp tối ưu hóa cụ thể này, nhưng điều này không có nghĩa là ngày nay phương pháp này không thể thực hiện được nữa. Điều đó chỉ có nghĩa là cần có nhiều cuộc đối thoại hơn giữa các lập trình viên và nhà thiết kế trò chơi và thường là sự can đảm để nói “Không” với các thách thức kỹ thuật. Cho dù bạn có muốn giải quyết chúng đến mức nào đi chăng nữa.

Nếu bạn đọc những tin đồn của tôi cho đến thời điểm này, bạn có thể theo dõi tôi tại Mastodon, Bluesky , hoặc LinkedIn hoặc đăng ký blog này ngay bên dưới bài viết này. Tôi xuất bản các bài viết mới về lập trình trò chơi, Unreal và phát triển trò chơi nói chung hàng tháng.

Tác giả: mariuz

#discussion