Rust開發遊戲三年後吐槽:上下文不靈活

banq發表於2024-04-27


這篇文章是一位遊戲開發者關於他們使用 Rust 進行遊戲開發的經歷和決定停止使用 Rust 的詳細闡述。文章中提到了他們對 Rust 語言和其社群的看法,以及他們為什麼認為 Rust 不適合他們的遊戲開發需求。

以下是文章的一些關鍵點:

  1. Rust 學習曲線和生產力:作者指出,儘管 Rust 社群經常告訴人們隨著經驗的增長,語言中的問題將會消失,但作者透過多年的使用和超過10萬行程式碼的編寫後發現,許多問題並沒有因為經驗豐富而消失。
  2. 借用檢查器(Borrow Checker):Rust 的借用檢查器會在最不方便的時候強制進行程式碼重構。作者認為這與編寫好程式碼的自然流程相沖突,因為好程式碼是透過迭代和嘗試不同的方法來完成的。
  3. 重構和迭代速度:作者強調,遊戲開發是一個不斷變化的複雜狀態機,需求經常變化。Rust 的靜態和過度檢查的特性與快速迭代和遊戲測試的需求相沖突。
  4. 間接性(Indirection):Rust 語言傾向於透過增加間接層來解決問題,但這通常會以犧牲開發者的舒適性為代價。
  5. ECS(Entity Component System):作者討論了 ECS 在 Rust 中的使用,指出很多人使用 ECS 是因為它解決了 Rust 借用檢查器的問題,而不一定需要 ECS 的效能優勢。
  6. 泛型系統和遊戲玩法:作者認為,過度的泛型系統會導致遊戲玩法變得無聊,因為它們缺乏特定的遊戲體驗。
  7. 全域性狀態(Global State):作者批評了 Rust 社群對全域性狀態的厭惡,認為對於遊戲開發來說,全域性狀態是有用且方便的。
  8. GUI 開發:文章指出 Rust 生態系統中缺乏優秀的遊戲 GUI 解決方案。
  9. 編譯時間:儘管 Rust 的編譯時間有所改善,但作者指出,當涉及到過程宏(procedural macros)時,編譯時間仍然是一個問題。
  10. 生態系統和炒作:作者批評了 Rust 遊戲開發生態系統,認為它更多是建立在炒作而不是實際釋出的專案上。
  11. 積極的方面:儘管文章主要關注了 Rust 的缺點,但作者也提到了一些積極的方面,如 Rust 的效能、列舉的實現、Rust 分析器和特質(traits)系統。
  12. 未來計劃:作者提到他們計劃將他們自己開發的 Comfy 遊戲引擎的渲染器移植到 Macroquad,這是一個更為實用和維護良好的庫。

文章最後,作者還提到了他們的新遊戲《Unrelaxing Quacks》,這是一個快節奏的生存遊戲,他們利用 Rust 能夠實現大量敵人和投射物的同時保持高效能。

這篇文章是作者個人對 Rust 在遊戲開發中應用的深入反思和經驗分享,提供了對 Rust 語言和社群的批判性看法,並且基於他們的開發經驗提出了一些建設性的批評和建議。


banq注:以上是國產大模型kimi的總結,但是沒有指出本文的真正關鍵點:

  • 上下文物件不夠靈活

由於 Rust 對程式設計師有一套相對獨特的限制,它最終會產生許多自找的問題,而這些問題的解決方案在其他語言中不一定經常出現。

其中一個例子就是會被傳遞的上下文物件
在幾乎所有其他語言中,以全域性變數或單子 Singleton 單例模式 的形式引入全域性狀態都不是什麼大問題。遺憾的是,由於上述種種原因,Rust 使得這種做法變得更加困難。

人們首先會想到的解決方案是 "只需儲存對任何需要的引用就可以了",但任何使用過幾天 Rust 的人都會意識到這是不可能的。借用檢查器需要跟蹤每個引用欄位的生命週期,而由於生命週期會成為泛型,毒害型別的每個使用點,因此這根本無法輕易進行實驗。

在許多情況下我們也不能只是“儲存對某物的引用”,因為生命週期是行不通的。

注意:生命週期 其實是 上下文 問題。

Rust 提供的一種替代方法是共享所有權,即 Rc<T> 或 Arc<T>。

  • 這當然也行得通,但卻很不受歡迎。
  • 在使用 Rust 一段時間後,我意識到的一件事是,使用這些方法實際上可以讓人少走彎路,儘管這需要你不再告訴你的 Rust 朋友你寫的程式碼,或者至少把它隱藏起來,假裝它不存在。

遺憾的是,在很多情況下,共享所有權並不是一個好的解決方案,這可能是出於效能方面的考慮,但有時你根本無法控制所有權,只能獲得一個引用。

Rust 遊戲開發中的第一大訣竅就是 "如果你在每一幀都自上而下地傳遞引用,那麼所有的生命週期/引用問題都會消失"。這實際上非常有效,與 React 自上而下傳遞道具的做法類似。

只有一個問題,那就是現在你需要把所有東西都傳遞給每個需要的函式。

起初,這似乎很明顯也很簡單,只要正確設計程式碼就不會有任何問題。哈哈,至少很多人都會這麼說,特別是 "如果你有這樣的問題,說明你的程式碼很醜/很爛/很差/很細條",或者 "你不應該這麼做",你知道,這些都是老生常談。

幸運的是,我們有一個實際的解決方案,那就是建立一個上下文結構體,該結構體會被傳遞幷包含所有這些引用。這個結構體有一個生命週期,但只有一個,最終看起來像這樣:
 

struct Context<'a> {
  player: &'a mut Player,
  camera: &'a mut Camera,
  <font>// ...<i>
}

這樣,遊戲中的每個函式都可以接受一個簡單的 c: &mut Context,並獲得所需的內容。

很棒,對吧?

但是,前提限制是:只要你不借用任何東西:
想象一下,你既想執行一個玩家系統,又想保留攝像機(兩個上下文都在借用同一個東西):

  • 玩家系統player_system 就像遊戲中的所有東西一樣,需要 c: &mut Context,因為你希望保持一致,避免傳遞 10 個不同的引數。
  • 但當你在攝像機上下文嘗試這樣做時

let cam = c.camera;

player_system(c);

cam.update();

你會得到 "不能借用 c,因為它已經被借用過了 "這樣的錯誤,因為我們已經接觸了一個欄位,而部分借用規則規定,如果你觸碰了一個東西,整個東西都會被借用。

幸運的是,Rust 並不完全是個傻瓜,它允許我們使用 player_system(c.player),因為部分借用允許我們借用不相關的欄位。

這時,借用檢查程式的維護者就會說,你只是設計錯了上下文物件,你應該把它拆分成多個上下文物件,或者根據使用情況對欄位進行分組,以便利用部分借用。也許所有攝像機的內容都在一個欄位中,所有播放器的內容都在另一個欄位中,然後我們只需將該欄位傳入 player_system 而不是整個 c,這樣大家就都滿意了,對嗎?

不幸的是,這屬於本文試圖解決的首要問題,即我想要做的是開發我的遊戲。我做遊戲的目的不是為了享受型別系統帶來的樂趣,也不是為了找出組織結構的最佳方法來讓編譯器滿意

重組上下文物件對我的單執行緒程式碼的可維護性沒有任何好處

我已經做過很多次這樣的事情了,我非常確定,下一次當我進行遊戲測試並得到新的遊戲建議時,我可能不得不再次更改設計。

banq注:上下文感知能力的缺失是很多程式設計師知識儲備中缺少的基礎知識。

這裡的問題是,程式碼被修改並不是因為業務邏輯發生了變化,而是因為編譯器對基本正確的東西不滿意。它可能不符合借用檢查器的工作方式,因為它只檢視型別,但它是正確的,因為如果我們傳遞我們正在使用的所有欄位,它就可以編譯得很好。Rust 讓我們在傳遞 7 個不同的引數或在任何時候重構我們的結構之間做出選擇,而這兩種選擇都是令人討厭和浪費時間的。

Rust 並沒有一個結構型別系統,我們可以說 "一個擁有這些欄位的型別",或者任何其他解決這個問題的方法,而無需重新定義結構和所有使用它的東西。它只是迫使程式設計師去做 "正確 "的事情。

banq注:Rust語言設計者沒有搞清楚:普通型別 與 上下文型別 區別


 

相關文章