用函數式編程在 JS 中開發遊戲

  • 2020 年 3 月 13 日
  • 筆記

一段時間以來,函數式編程範式比較火熱,並且在互聯網上有很多關於它的精彩書籍和文章,但是要找到相關程式的真實示例並不容易。因此,我決定嘗試使用 Javascript(當今最流行的程式語言)並遵循其概念創建一款遊戲。在本文中,我將分享一些經驗,並告訴你是否值得。

什麼是函數式編程?

簡而言之,函數式編程(FP)是試圖重現數學函數概念的範式,數學概念是域集(有效輸入)和共域(有效輸出)之間的關係。數學函數的輸出始終僅與一個輸入相關,因此,只要使用相同的輸入來計算數學函數,它就會返回相同的輸出。這是函數式編程最重要的概念之一,也稱為確定性

不確定函數示例

1let x = 1  2const nonDeterministicAdd = y => x + y  3nonDeterministicAdd(2) // 3  4x = 2  5nonDeterministicAdd(2) // 4

確定性函數示例

1const deterministicAdd = (x, y) => x + y  2deterministicAdd(1, 2) // 3

除了確定性之外,FP 中的函數還尋求不引起超出其範圍的修改。這些類型的功能稱為 pure。最後但並非最不重要的一點是,FP 中的數據必須是不可變的,這意味著創建後不能更改其值。這些概念使測試、快取和並行性更加容易。

除了這些基本概念之外,我還嘗試在遊戲開發期間使用無點樣式,該樣式能夠使程式碼更簡潔,因為它省略了不必要的參數和參數的使用。以下兩個鏈接給你提供了很好的參考。

  • https://jrsinclair.com/articles/2016/marvellously-mysterious-javascript-maybe-monad/#pointfreestyle
  • https://www.freecodecamp.org/news/how-point-free-composition-will-make-you-a-better-functional-programmer-33dcb910303a/

這個項目是一個在瀏覽器中運行的遊戲。因為 Javascript(JS)是我很熟悉的一種語言,並且是一種多範式語言,所以我選擇它為項目語言。

我推薦兩本關於 FP 的優秀書籍:

  • Functional Light JS
  • Mostly adequate guide to FP

項目

我們的項目是一個基於回合制的太空飛船遊戲。在遊戲中,每個玩家有 3 艘飛船,並且每回合必須選擇他們要在其可達範圍內移動飛船的位置以及要朝哪個方向射擊。當飛船被射中時,它將失去部分防護罩。如果宇宙飛船沒有防護罩將被摧毀,失去所有宇宙飛船的玩家將輸掉比賽。

比賽的初始輪

到目前為止,該遊戲僅允許一個玩家參與,並且控制螢幕頂部的 3 個太空飛船,去對抗一個控制底部 3 個太空飛船的腳本,該腳本將其太空飛船的位置和目標隨機化。關於圖形部分,我使用了 PixiJS 程式包來控制渲染,這是該項目唯一的依賴項,並且我還使用了從OpenGameart 網站上的 UnLucky Studio 免費獲得的太空飛船精靈 。

基礎和輔助函數

在開始,我們先創建一個文件,其中包含幾乎所有項目文件中都會用到的基本函數。其中一些基本函數是 JS 固有的,例如 map 和 reduce。JS還有一些其他功能,它們通過不更改輸入值而適合FP範例,並且已在項目中使用,例如 filter, find, some, every。發現這些功能的一個很好的來源是Does it mutate。要遵循無點樣式,還必須實現以下基本函數:

  • Curry:允許函數在單獨的時刻接收其參數
1const add = curry((x, y) => x + y)  2add(1, 2) // 3  3add(1)(2) // 3
  • Compose:函數作為參數傳遞並以相反的順序執行。每個函數消耗前一個函數的返回值。
1const addAndIncrement = compose(  2   add(1), // previous add result + 1  3   add // arg1 + arg2  4)  5addAndIncrement(2, 2) // 5

已經在其中實現了這些函數的幾個庫,例如 Ramda,但是在這個項目中,我決定實現它們以試圖更好地理解它們的工作原理。這篇文章(https://medium.com/dailyjs/functional-js-with-es6-recursive-patterns-b7d0813ef9e3) 是研究它們如何工作以及如何遞歸實現這些功能的重要資料。

為了簡化所使用的本機 JS 函數的構成,我使用 curry 創建了helper,其中條目作為參數傳遞。

例:

1const filter = curry((fn, array) => array.filter(fn))  2const getAliveSpaceships =  3    compose(  4        filter(isAlive),  5        getSpaceships

我們如何聲明模型?

關於模型的實現,我們使用了 functional-shared 樣式,其中模型實例是具有其屬性和函數的對象。為了管理模型的狀態,我們創建了以下 helper,其中 getState 返回實例的狀態。assignState 返回一個新實例,舊狀態與新實例連接在一起,getProp 返回封裝在 monad 中的傳遞屬性的值。Monad 在函數式中是一種流行的構造,並且很難總結出一個簡介的定義,這篇文章對其做了一個很好的解釋:https://jrsinclair.com/articles/2016/marvellously-mysterious-javascript-maybe-monad/。

1const modelFunctions = (model, state) => ({  2    getState: () => state,  3    assignState: newProps => model({ ...state, ...newProps }),  4    getProp: name => getProp(state, name),  5})

使用這個 helper,我們可以聲明模型、創建實例並使用其函數,如下所示:

1const Engine = state => ({ ...modelFunctions(Engine, state) })  2Engine({ a: 'a' }).assignState({ b: 'b' }).getState() // { a: 'a', b: 'b' }

實現其餘部分

定義了基本函數和模板後,仍有許多工作要做。下面是項目的其它一些函數,這些函數的可讀性很好。

  • 移除玩家被摧毀的飛船
1const removeDestroyedSpaceships = player => compose(  2    setSpaceships(player),  3    getAliveSpaceships  4)(player) 
  • 減少飛船的護罩
1export const reduceShield = curry((spaceship, damage) =>  2    compose(  3        checkDestroyed,  4        shield => assignState({ shield }, spaceship),  5        shield => sub(shield, damage),  6        getShield  7    )(spaceship)  8)

與命令式編程相比,通過組合實現的程式碼通常更易於理解。例如我用 SonarQube 分析了此函數的認知複雜性,並獲得了最高分。

  • 獲取飛船的子彈
1export const getBullets = compose(  2    either([]),  3    getProp('bullets')  4)

在這裡可以省略函數參數,因為它僅由複合函數使用。還可以保證返回的值將是有效的,因為 getProp 返回一個 monad,而 either 返回一個 monad 的封裝值(如果它是有效值或空數組)。

  • 為子彈設置新的位置
1const setPosition = curry((coordinate, bullet) =>  2    compose(  3        callListenerIfExist('onMove'),  4        assignState({ coordinate })  5    )(bullet)  6)

函數式編程的組合要求函數始終具有返回值。如果 callListenerIfExist 未返回任何值,則執行後將無法與其他函數或 setPosition 鏈接其他函數。

它值得嗎?

這是項目的github 存儲庫(https://github.com/karranb/functional-spaceship-game),並託管在此這裡(https://zealous-lichterman-adc5bd.netlify.com/)。因為我以前沒有使用函數式編程的經驗,所以我不得不多次重構項目,並且由於堆棧跟蹤限制之類的原因,還發現函數式難以調試。但是另一方面,這些函數有很高的可讀性,並且易於重用。我不建議使用雄心勃勃的範式或技術來完成那些需要在最後期限之前完成的項目,但是該項目是出於學習目的而開發的。避免使用庫並實現基本函數對於了解每個庫的工作方式非常有幫助,最終的包大小几乎僅是所使用的 PixiJS 模組的大小。

原文鏈接

https://cheesecakelabs.com/blog/functional-programming-game-js/