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

做者:Karran Besenjavascript

翻譯:瘋狂的技術宅前端

原文:cheesecakelabs.com/blog/functi…java

未經容許嚴禁轉載git

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

什麼是函數式編程?

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

不肯定函數示例編程

let x = 1
const nonDeterministicAdd = y => x + y
nonDeterministicAdd(2) // 3
x = 2
nonDeterministicAdd(2) // 4
複製代碼

肯定性函數示例前端工程化

const deterministicAdd = (x, y) => x + y
deterministicAdd(1, 2) // 3
複製代碼

除了肯定性以外,FP 中的函數還尋求不引發超出其範圍的修改。這些類型的功能稱爲 pure。最後但並不是最不重要的一點是,FP 中的數據必須是不可變的,這意味着建立後不能更改其值。這些概念使測試、緩存和並行性更加容易。數組

除了這些基本概念以外,我還嘗試在遊戲開發期間使用無點樣式,該樣式可以使代碼更簡潔,由於它省略了沒必要要的參數和參數的使用。如下兩個連接給你提供了很好的參考。瀏覽器

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

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

項目

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

比賽的初始輪

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

基礎和輔助函數

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

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

已經在其中實現了這些函數的幾個庫,例如 Ramda,可是在這個項目中,我決定實現它們以試圖更好地理解它們的工做原理。這篇文章(medium.com/dailyjs/fun…) 是研究它們如何工做以及如何遞歸實現這些功能的重要資料。

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

例:

const filter = curry((fn, array) => array.filter(fn))
const getAliveSpaceships =
    compose(
        filter(isAlive),
        getSpaceships
複製代碼

咱們如何聲明模型?

關於模型的實現,咱們使用了 functional-shared 樣式,其中模型實例是具備其屬性和函數的對象。爲了管理模型的狀態,咱們建立了如下 helper,其中 getState 返回實例的狀態。 assignState 返回一個新實例,舊狀態與新實例鏈接在一塊兒,getProp 返回封裝在 monad 中的傳遞屬性的值。 Monad 在函數式中是一種流行的構造,而且很難總結出一個簡介的定義,這篇文章對其作了一個很好的解釋:jrsinclair.com/articles/20…

const modelFunctions = (model, state) => ({
    getState: () => state,
    assignState: newProps => model({ ...state, ...newProps }),
    getProp: name => getProp(state, name),
})
複製代碼

使用這個 helper,咱們能夠聲明模型、建立實例並使用其函數,以下所示:

const Engine = state => ({ ...modelFunctions(Engine, state) })
Engine({ a: 'a' }).assignState({ b: 'b' }).getState() // { a: 'a', b: 'b' }
複製代碼

實現其他部分

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

  • 移除玩家被摧毀的飛船
const removeDestroyedSpaceships = player => compose(
    setSpaceships(player),
    getAliveSpaceships
)(player) 
複製代碼
  • 減小飛船的護罩
export const reduceShield = curry((spaceship, damage) =>
    compose(
        checkDestroyed,
        shield => assignState({ shield }, spaceship),
        shield => sub(shield, damage),
        getShield
    )(spaceship)
)
複製代碼

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

  • 獲取飛船的子彈
export const getBullets = compose(
    either([]),
    getProp('bullets')
)
複製代碼

在這裏能夠省略函數參數,由於它僅由複合函數使用。還能夠保證返回的值將是有效的,由於 getProp 返回一個 monad,而 either 返回一個 monad 的封裝值(若是它是有效值或空數組)。

  • 爲子彈設置新的位置
const setPosition = curry((coordinate, bullet) =>
    compose(
        callListenerIfExist('onMove'),
        assignState({ coordinate })
    )(bullet)
)
複製代碼

函數式編程的組合要求函數始終具備返回值。若是 callListenerIfExist 未返回任何值,則執行後將沒法與其餘函數或 setPosition 連接其餘函數。

它值得嗎?

這是項目的github 存儲庫,並託管在此這裏(zealous-lichterman-adc5bd.netlify.com/)。由於我之前沒有使用… PixiJS 模塊的大小。

歡迎關注前端公衆號:前端先鋒,免費領取前端工程化實用工具包。

相關文章
相關標籤/搜索