<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      ECS模式

      大家好,本文提出了ECS模式。ECS模式是游戲引擎中常用的模式,通常用來組織游戲場(chǎng)景。本文出自我寫的開源書《3D編程模式》,該書的更多內(nèi)容請(qǐng)?jiān)斠姡?br> Github
      在線閱讀

      目錄

      普通英雄和超級(jí)英雄

      需求

      我們需要開發(fā)一個(gè)游戲,游戲中有兩種人物:普通英雄和超級(jí)英雄,他們具有下面的行為:

      • 普通英雄只能移動(dòng)
      • 超級(jí)英雄不僅能夠移動(dòng),還能飛行

      我們使用下面的方法來渲染:

      • 使用Instance技術(shù)來一次性批量渲染所有的普通英雄
      • 一個(gè)一個(gè)地渲染每個(gè)超級(jí)英雄

      實(shí)現(xiàn)思路

      應(yīng)該有一個(gè)游戲世界,它由多個(gè)普通英雄和多個(gè)超級(jí)英雄組成

      一個(gè)模塊對(duì)應(yīng)一個(gè)普通英雄,一個(gè)模塊對(duì)應(yīng)一個(gè)超級(jí)英雄。模塊應(yīng)該維護(hù)該英雄的數(shù)據(jù)和實(shí)現(xiàn)該英雄的行為

      給出UML

      領(lǐng)域模型

      image

      總體來看,領(lǐng)域模型分為用戶、游戲世界、英雄這三個(gè)部分

      我們看下用戶、游戲世界這兩個(gè)部分:

      Client是用戶

      World是游戲世界,由多個(gè)普通英雄和多個(gè)超級(jí)英雄組成。World負(fù)責(zé)管理所有的英雄,并且實(shí)現(xiàn)了初始化和主循環(huán)的邏輯

      我們看下英雄這個(gè)部分:

      一個(gè)NormalHero對(duì)應(yīng)一個(gè)普通英雄,維護(hù)了該英雄的數(shù)據(jù),實(shí)現(xiàn)了移動(dòng)的行為

      一個(gè)SuperHero對(duì)應(yīng)一個(gè)超級(jí)英雄, 維護(hù)了該英雄的數(shù)據(jù),實(shí)現(xiàn)了移動(dòng)、飛行的行為

      給出代碼

      首先,我們看下Client的代碼;
      然后,我們依次看下Client代碼中前兩個(gè)步驟的代碼,它們包括:

      • 創(chuàng)建WorldState的代碼
      • 創(chuàng)建場(chǎng)景的代碼

      然后,因?yàn)閯?chuàng)建場(chǎng)景時(shí)操作了普通英雄和超級(jí)英雄,所以我們看下它們的代碼,它們包括:

      • 普通英雄移動(dòng)的代碼
      • 超級(jí)英雄移動(dòng)和飛行的代碼

      然后,我們依次看下Client代碼中剩余的兩個(gè)步驟的代碼,它們包括:

      • 初始化的代碼
      • 主循環(huán)的代碼

      然后,我們看下主循環(huán)的一幀中每個(gè)步驟的代碼,它們包括:

      • 主循環(huán)中更新的代碼
      • 主循環(huán)中渲染的代碼

      最后,我們運(yùn)行Client的代碼

      Client的代碼

      Client

      let worldState = World.createState()
      
      worldState = _createScene(worldState)
      
      worldState = WorldUtils.init(worldState)
      
      WorldUtils.loop(worldState, [World.update, World.renderOneByOne, World.renderInstances])
      

      Client首先創(chuàng)建了WorldState,用來保存游戲世界中所有的數(shù)據(jù);然后創(chuàng)建了場(chǎng)景;然后進(jìn)行了初始化;最后開始了主循環(huán)

      創(chuàng)建WorldState的代碼

      World

      export let createState = (): worldState => {
          return {
              normalHeroes: Map(),
              superHeroes: Map()
          }
      }
      

      createState函數(shù)創(chuàng)建了WorldState,它包括兩個(gè)分別用來保存所有的普通英雄和所有的超級(jí)英雄的容器

      創(chuàng)建場(chǎng)景的代碼

      Client

      let _createScene = (worldState: worldState): worldState => {
          創(chuàng)建和加入normalHero1到worldState.normalHeroes
          創(chuàng)建和加入normalHero2到worldState.normalHeroes
      
          normalHero1移動(dòng)
      
          創(chuàng)建和加入superHero1到worldState.superHeroes
          創(chuàng)建和加入superHero2到worldState.superHeroes
      
          superHero1移動(dòng)
          superHero1飛行
      
          return worldState
      }
      

      _createScene函數(shù)創(chuàng)建了場(chǎng)景,創(chuàng)建和加入了兩個(gè)普通英雄和兩個(gè)超級(jí)英雄到游戲世界中。其中第一個(gè)普通英雄進(jìn)行了移動(dòng),第一個(gè)超級(jí)英雄進(jìn)行了移動(dòng)和飛行

      NormalHero

      //創(chuàng)建一個(gè)普通英雄
      export let create = (): [normalHeroState, normalHero] => {
          創(chuàng)建它的state數(shù)據(jù):
              position設(shè)置為[0,0,0]
              velocity設(shè)置為1.0
      
              其中:position為位置,velocity為速度
      
          返回該英雄
      }
      

      NormalHero的create函數(shù)創(chuàng)建了一個(gè)普通英雄,初始化了它的數(shù)據(jù)

      SuperHero

      //創(chuàng)建一個(gè)超級(jí)英雄
      export let create = (): [superHeroState, superHero] => {
          創(chuàng)建它的state數(shù)據(jù):
              position設(shè)置為[0,0,0]
              velocity設(shè)置為1.0
              maxVelocity設(shè)置為1.0
      
              其中:position為位置,velocity為速度,maxVelocity為最大速度
      
          返回該英雄
      }
      

      SuperHero的create函數(shù)創(chuàng)建了一個(gè)超級(jí)英雄,初始化了它的數(shù)據(jù)

      普通英雄移動(dòng)的代碼

      NormalHero

      //一個(gè)普通英雄的移動(dòng)
      export let move = (worldState: worldState, normalHero: normalHero): worldState => {
          從worldState中獲得該英雄的position和velocity
      
          根據(jù)velocity,更新position
      
          更新worldState中該英雄的數(shù)據(jù)
      }
      

      move函數(shù)實(shí)現(xiàn)了移動(dòng)的行為邏輯,更新了位置

      超級(jí)英雄移動(dòng)和飛行的代碼

      SuperHero

      //一個(gè)超級(jí)英雄的移動(dòng)
      export let move = (worldState: worldState, superHero: superHero): worldState => {
          從worldState中獲得該英雄的position和velocity
      
          根據(jù)velocity,更新position
      
          更新worldState中該英雄的數(shù)據(jù)
      }
      
      //一個(gè)超級(jí)英雄的飛行
      export let fly = (worldState: worldState, superHero: superHero): worldState => {
          從worldState中獲得該英雄的position和velocity、maxVelocity
      
          根據(jù)maxVelocity、velocity,更新position
      
          更新worldState中該英雄的數(shù)據(jù)
      }
      

      SuperHero的move函數(shù)的邏輯跟NormalHero的move函數(shù)的邏輯是一樣的

      fly函數(shù)實(shí)現(xiàn)了飛行的行為邏輯。它跟move函數(shù)一樣,也是更新英雄的position。只是因?yàn)閮烧咴谟?jì)算時(shí)使用的速度的算法不一樣,所以更新position的幅度不同

      初始化的代碼

      WorldUtils

      export let init = (worldState) => {
          console.log("初始化...")
      
          return worldState
      }
      

      init函數(shù)實(shí)現(xiàn)了初始化。這里沒有任何邏輯,只是進(jìn)行了打印

      主循環(huán)的代碼

      WorldUtils

      export let loop = (worldState, [update, renderOneByOne, renderInstances]) => {
          worldState = update(worldState)
          renderOneByOne(worldState)
          renderInstances(worldState)
      
          ...
      
          requestAnimationFrame(
              (time) => {
                  loop(worldState, [update, renderOneByOne, renderInstances])
              }
          )
      }
      

      loop函數(shù)實(shí)現(xiàn)了主循環(huán)。在主循環(huán)的一幀中,首先進(jìn)行了更新;然后一個(gè)一個(gè)地渲染了所有的超級(jí)英雄;然后一次性批量渲染了所有的普通英雄;最后執(zhí)行下一幀

      主循環(huán)中更新的代碼

      World

      export let update = (worldState: worldState): worldState => {
          遍歷worldState.normalHeroes:
              更新每個(gè)normalHero
          遍歷worldState.superHeroes:
              更新每個(gè)superHero
      }
      

      update函數(shù)實(shí)現(xiàn)了更新,它會(huì)遍歷所有的normalHero和superHero,調(diào)用它們的update函數(shù)來更新自己

      我們看下NormalHero的update函數(shù)的代碼:

      //更新一個(gè)普通英雄
      export let update = (normalHeroState: normalHeroState): normalHeroState => {
          更新該英雄的position
      }
      

      它更新了自己的position

      我們看下SuperHero的update函數(shù)的代碼:

      //更新一個(gè)超級(jí)英雄
      export let update = (superHeroState: superHeroState): superHeroState => {
          更新該英雄的position
      }
      

      它的邏輯跟NormalHero的update是一樣的,這是因?yàn)閮烧叨际褂猛瑯拥乃惴▉砀伦约旱膒osition

      主循環(huán)中渲染的代碼

      World

      export let renderOneByOne = (worldState: worldState): void => {
          worldState.superHeroes.forEach(superHeroState => {
              console.log("OneByOne渲染 SuperHero...")
          })
      }
      
      export let renderInstances = (worldState: worldState): void => {
          let normalHeroStates = worldState.normalHeroes
      
          console.log("批量Instance渲染 NormalHeroes...")
      }
      

      renderOneByOne函數(shù)實(shí)現(xiàn)了超級(jí)英雄的渲染,它遍歷每個(gè)超級(jí)英雄,一個(gè)一個(gè)地渲染
      renderInstances函數(shù)實(shí)現(xiàn)了普通英雄的渲染,它一次性獲得所有的普通英雄,批量渲染

      運(yùn)行Client的代碼

      下面,我們運(yùn)行Client的代碼,打印的結(jié)果如下:

      初始化...
      更新NormalHero
      更新NormalHero
      更新SuperHero
      更新SuperHero
      OneByOne渲染 SuperHero...
      OneByOne渲染 SuperHero...
      批量Instance渲染 NormalHeroes...
      {"normalHeroes":{"144891":{"position":[0,0,0],"velocity":1},"648575":{"position":[2,2,2],"velocity":1}},"superHeroes":{"497069":{"position":[6,6,6],"velocity":1,"maxFlyVelocity":10},"783438":{"position":[0,0,0],"velocity":1,"maxFlyVelocity":10}}}
      

      通過打印的數(shù)據(jù),可以看到運(yùn)行的步驟如下:
      1.進(jìn)行了初始化
      2.更新了所有的人物,包括兩個(gè)普通英雄和兩個(gè)超級(jí)英雄
      3.渲染了2個(gè)超級(jí)英雄
      4.一次性批量渲染了所有的普通英雄
      5.打印了WorldState

      我們看下打印的WorldState:

      • WorldState的normalHeroes中一共有兩個(gè)普通英雄的數(shù)據(jù),其中有一個(gè)普通英雄數(shù)據(jù)的position為[2,2,2]而不是初始的[0,0,0],說明該普通英雄進(jìn)行了移動(dòng)操作;
      • WorldState的superHeroes中一共有兩個(gè)超級(jí)英雄的數(shù)據(jù),其中有一個(gè)超級(jí)英雄數(shù)據(jù)的position為[6,6,6],說明該超級(jí)英雄進(jìn)行了移動(dòng)和飛行操作

      值得注意的是:
      因?yàn)閃orldState的normalHeroes和superHeroes中的Key是隨機(jī)生成的id值,所以每次打印時(shí)Key都不一樣

      提出問題

      • NormalHero和SuperHero中的update、move函數(shù)的邏輯是重復(fù)的

      • 如果英雄增加更多的行為,NormalHero和SuperHero模塊會(huì)越來越復(fù)雜,不容易維護(hù)

      雖然這兩個(gè)問題都可以通過繼承來解決,即最上面是Hero基類,然后不同種類的Hero層層繼承,但是繼承的方式很死板,不夠靈活

      基于組件化的思想改進(jìn)

      概述解決方案

      • 基于組件化的思想,用組合代替繼承。具體修改如下:

        • 將人物抽象為GameObject;
        • 將人物的行為抽象為組件,并把人物的相關(guān)數(shù)據(jù)也移到組件中;
        • GameObject通過掛載不同的組件,來實(shí)現(xiàn)不同的行為

      這樣就通過GameObject組合不同的組件來代替人物層層繼承,從而更加靈活

      給出UML

      領(lǐng)域模型

      image

      總體來看,領(lǐng)域模型分為用戶、游戲世界、GameObject、組件這四個(gè)部分

      我們看下用戶、游戲世界這兩個(gè)部分:

      Client是用戶

      World是游戲世界,由多個(gè)GameObject組成。World負(fù)責(zé)管理所有的GameObject,并且實(shí)現(xiàn)了初始化和主循環(huán)的邏輯

      我們看下GameObject這個(gè)部分:

      一個(gè)GameObject對(duì)應(yīng)一個(gè)人物。GameObject負(fù)責(zé)管理掛載的組件,它可以掛載PositionComponent、VelocityComponent、FlyComponent、InstanceComponent這四種組件,每種組件最多掛載一個(gè)

      我們看下組件這個(gè)部分:

      組件負(fù)責(zé)維護(hù)自己的數(shù)據(jù),實(shí)現(xiàn)自己的行為邏輯。具體來說,是將NormalHero、SuperHero的position數(shù)據(jù)和move函數(shù)、update函數(shù)移到了PositionComponent中;將NormalHero、SuperHero的velocity數(shù)據(jù)移到了VelocityComponent中;將SuperHero的maxVelocity數(shù)據(jù)和fly函數(shù)移到了FlyComponent中

      InstanceComponent沒有數(shù)據(jù)和邏輯,它只是一個(gè)標(biāo)記,用來表示掛載該組件的GameObject使用一次性批量渲染的算法來渲染

      結(jié)合UML圖,描述如何具體地解決問題

      • 現(xiàn)在只需要實(shí)現(xiàn)一次Position組件中的update、move函數(shù),然后將它掛載到不同的GameObject中,就可以實(shí)現(xiàn)普通英雄和超級(jí)英雄的更新、移動(dòng)的邏輯,從而消除了之前在NormalHero、SuperHero中因共實(shí)現(xiàn)了兩次的update、move函數(shù)而造成的重復(fù)代碼

      • 因?yàn)镹ormalHero、SuperHero都是GameObject,而GameObject本身只負(fù)責(zé)管理組件,沒有行為邏輯,所以隨著人物的行為的增加,GameObject并不會(huì)增加邏輯,而只需要增加對(duì)應(yīng)行為的組件,讓GameObject掛載該組件即可
        通過這樣的設(shè)計(jì),將行為的邏輯和數(shù)據(jù)從人物移到了組件中,從而可以通過組合的方式使人物具有多個(gè)行為,避免了龐大的人物模塊的出現(xiàn)

      給出代碼

      首先,我們看下Client的代碼;
      然后,我們依次看下Client代碼中前兩個(gè)步驟的代碼,它們包括:

      • 創(chuàng)建WorldState的代碼
      • 創(chuàng)建場(chǎng)景的代碼

      然后,因?yàn)閯?chuàng)建場(chǎng)景時(shí)操作了普通英雄和超級(jí)英雄,所以我們看下它們的代碼,它們包括:

      • 移動(dòng)的相關(guān)代碼
      • 飛行的相關(guān)代碼

      然后,我們依次看下Client代碼中剩余的兩個(gè)步驟的代碼,它們包括:

      • 初始化和主循環(huán)的代碼

      然后,我們看下主循環(huán)的一幀中每個(gè)步驟的代碼,它們包括:

      • 主循環(huán)中更新的代碼
      • 主循環(huán)中渲染的代碼

      最后,我們運(yùn)行Client的代碼

      Client的代碼

      Client的代碼跟之前的Client的代碼基本上一樣,故省略。不一樣的地方是_createScene函數(shù)中創(chuàng)建場(chǎng)景的方式不一樣,這個(gè)等會(huì)再討論

      創(chuàng)建WorldState的代碼

      World

      export let createState = (): worldState => {
          return {
              gameObjects: Map()
          }
      }
      

      createState函數(shù)創(chuàng)建了WorldState,它保存了一個(gè)用來保存所有的gameObject的容器

      創(chuàng)建場(chǎng)景的代碼

      Client

      let _createScene = (worldState: worldState): worldState => {
          創(chuàng)建和加入normalHero1到worldState.gameObjects:
              創(chuàng)建gameObject
              創(chuàng)建positionComponent
              創(chuàng)建velocityComponent
              創(chuàng)建instanceComponent
              掛載positionComponent、velocityComponent、instanceComponent到gameObject
              加入gameObject到worldState.gameObjects
      
          創(chuàng)建和加入normalHero2到worldState.gameObjects
      
          normalHero1移動(dòng):
              調(diào)用normalHero1掛載的positionComponent的move函數(shù)
      
          創(chuàng)建和加入superHero1到worldState.gameObjects:
              創(chuàng)建gameObject
              創(chuàng)建positionComponent
              創(chuàng)建velocityComponent
              創(chuàng)建flyComponent
              掛載positionComponent、velocityComponent、flyComponent到gameObject
              加入gameObject到worldState.gameObjects
      
          創(chuàng)建和加入superHero2到worldState.gameObjects
      
          superHero1移動(dòng):
              調(diào)用superHero1掛載的positionComponent的move函數(shù)
          superHero1飛行:
              調(diào)用superHero1掛載的flyComponent的fly函數(shù)
      
          return worldState
      }
      
      

      _createScene函數(shù)創(chuàng)建了場(chǎng)景,場(chǎng)景的內(nèi)容跟之前一樣,都包括了2個(gè)普通英雄和2個(gè)超級(jí)英雄,只是現(xiàn)在創(chuàng)建一個(gè)英雄的方式改變了,具體變?yōu)椋菏紫葎?chuàng)建一個(gè)GameObject和相關(guān)的組件;然后掛載組件到GameObject;最后加入該GameObject到World中

      普通英雄對(duì)應(yīng)的GameObject掛載的組件跟超級(jí)英雄對(duì)應(yīng)的GameObject掛載的組件也不一樣,其中前者掛載了InstanceComponent(因?yàn)槠胀ㄓ⑿坌枰淮涡耘夸秩荆笳邉t掛載了FlyComponent(因?yàn)槌?jí)英雄多出了飛行的行為)

      另外,現(xiàn)在改為通過調(diào)用對(duì)應(yīng)組件的函數(shù)而不是直接操作英雄模塊,從而實(shí)現(xiàn)英雄的“移動(dòng)”、“飛行”

      GameObject

      //創(chuàng)建一個(gè)gameObject
      export let create = (): [gameObjectState, gameObject] => {
          創(chuàng)建它的state數(shù)據(jù):
              沒有掛載任何的組件
      
          返回該gameObject
      }
      

      GameObject的create函數(shù)創(chuàng)建了一個(gè)gameObject,初始化了它的數(shù)據(jù)

      PositionComponent

      //創(chuàng)建一個(gè)positionComponent
      export let create = (): positionComponentState => {
          創(chuàng)建它的state數(shù)據(jù):
              gameObject設(shè)置為null
              position設(shè)置為[0,0,0]
      
              其中:position為位置,gameObject為掛載到的gameObject
      
          返回該組件
      }
      

      PositionComponent的create函數(shù)創(chuàng)建了一個(gè)positionComponent,初始化了它的數(shù)據(jù)

      VelocityComponent

      //創(chuàng)建一個(gè)velocityComponent
      export let create = (): velocityComponentState => {
          創(chuàng)建它的state數(shù)據(jù):
              gameObject設(shè)置為null
              velocity設(shè)置為1.0
      
              其中:velocity為速度,gameObject為掛載到的gameObject
      
          返回該組件
      }
      

      FlyComponent

      //創(chuàng)建一個(gè)flyComponent
      export let create = (): flyComponentState => {
          創(chuàng)建它的state數(shù)據(jù):
              gameObject設(shè)置為null
              maxVelocity設(shè)置為1.0
      
              其中:maxVelocity為最大速度,gameObject為掛載到的gameObject
      
          返回該組件
      }
      

      InstanceComponent

      //創(chuàng)建一個(gè)instanceComponent
      export let create = (): instanceComponentState => {
          創(chuàng)建它的state數(shù)據(jù):
              gameObject設(shè)置為null
      
              其中:gameObject為掛載到的gameObject
      
          返回該組件
      }
      

      這三種組件的create函數(shù)的職責(zé)跟PositionComponent的create函數(shù)的職責(zé)一樣,不一樣的是InstanceComponent的state數(shù)據(jù)中只有掛載到的gameObject,沒有自己的數(shù)據(jù)

      我們可以看到,組件的state數(shù)據(jù)中都保存了掛載到的gameObject,這樣做的目的是可以通過它來獲得掛載到它上的其它組件,從而一個(gè)組件可以操作其它掛載的組件

      移動(dòng)的相關(guān)代碼

      PositionComponent

      ...
      
      //獲得一個(gè)組件的position
      export let getPosition = (positionComponentState: positionComponentState) => {
          return positionComponentState.position
      }
      
      //設(shè)置一個(gè)組件的position
      export let setPosition = (positionComponentState: positionComponentState, position) => {
          return {
              ...positionComponentState,
              position: position
          }
      }
      
      ...
      
      //一個(gè)gameObject的移動(dòng)
      export let move = (worldState: worldState, positionComponentState: positionComponentState): worldState => {
          //獲得該組件的position、gameObject
          let [x, y, z] = getPosition(positionComponentState)
      
          //通過該組件的gameObject,獲得掛載到該gameObject的velocityComponent組件
          //獲得它的velocity
          let gameObject = getExnFromStrictNull(positionComponentState.gameObject)
          let velocity = VelocityComponent.getVelocity(GameObject.getVelocityComponentExn(getGameObjectStateExn(worldState, gameObject)))
      
          //根據(jù)velocity,更新該組件的position
          positionComponentState = setPosition(positionComponentState, [x + velocity, y + velocity, z + velocity])
      
          更新worldState中該組件掛載的gameObject中的該組件的數(shù)據(jù)
      }
      

      VelocityComponent

      //獲得一個(gè)組件的velocity
      export let getVelocity = (velocityComponentState: velocityComponentState) => {
          return velocityComponentState.velocity
      }
      

      PositionComponent維護(hù)了position數(shù)據(jù),提供了它的get、set函數(shù)。VelocityComponent維護(hù)了velocity數(shù)據(jù),,提供了它的get函數(shù)

      另外,PositionComponent的move函數(shù)實(shí)現(xiàn)了移動(dòng)的行為邏輯

      飛行的相關(guān)代碼

      FlyComponent

      //獲得一個(gè)組件的maxVelocity
      export let getMaxVelocity = (flyComponentState: flyComponentState) => {
          return flyComponentState.maxVelocity
      }
      
      //設(shè)置一個(gè)組件的maxVelocity
      export let setMaxVelocity = (flyComponentState: flyComponentState, maxVelocity) => {
          return {
              ...flyComponentState,
              maxVelocity: maxVelocity
          }
      }
      
      //一個(gè)gameObject的飛行
      export let fly = (worldState: worldState, flyComponentState: flyComponentState): worldState => {
          //獲得該組件的maxVelocity、gameObject
          let maxVelocity = getMaxVelocity(flyComponentState)
          let gameObject = getExnFromStrictNull(flyComponentState.gameObject)
      
          //通過該組件的gameObject,獲得掛載到該gameObject的positionComponent組件
          //獲得它的position
          let [x, y, z] = PositionComponent.getPosition(GameObject.getPositionComponentExn(getGameObjectStateExn(worldState, gameObject)))
      
          //通過該組件的gameObject,獲得掛載到該gameObject的velocityComponent組件
          //獲得它的velocity
          let velocity = VelocityComponent.getVelocity(GameObject.getVelocityComponentExn(getGameObjectStateExn(worldState, gameObject)))
      
          //根據(jù)maxVelocity、velocity,更新positionComponent組件的position
          velocity = velocity < maxVelocity ? (velocity * 2.0) : maxVelocity
          let positionComponentState = PositionComponent.setPosition(GameObject.getPositionComponentExn(getGameObjectStateExn(worldState, gameObject)), [x + velocity, y + velocity, z + velocity])
      
          更新worldState中該組件掛載的gameObject中的該組件的數(shù)據(jù)
      }
      

      FlyComponent維護(hù)了maxVelocity數(shù)據(jù),,提供了它的get、set函數(shù)。另外,F(xiàn)lyComponent的fly函數(shù)實(shí)現(xiàn)了飛行的行為邏輯

      初始化和主循環(huán)的代碼

      初始化和主循環(huán)的邏輯跟之前一樣,故省略代碼

      主循環(huán)中更新的代碼

      World

      export let update = (worldState: worldState): worldState => {
          遍歷worldState.gameObjects:
              if(gameObject掛載了positionComponent){
                  更新positionComponent
              }
      }
      

      update函數(shù)實(shí)現(xiàn)了更新,它會(huì)遍歷所有的gameObject,調(diào)用它掛載的PositionComponent組件的update函數(shù)來更新該組件。我們看下PositionComponent的update函數(shù)的代碼:

      //更新一個(gè)組件
      export let update = (positionComponentState: positionComponentState): positionComponentState => {
          更新該組件的position
      }
      

      它的邏輯跟之前的NormalHero和SuperHero中的update函數(shù)的邏輯是一樣的

      主循環(huán)中渲染的代碼

      World

      export let renderOneByOne = (worldState: worldState): void => {
          let superHeroGameObjects = worldState.gameObjects.filter(gameObjectState => {
              //判斷gameObject是不是沒有掛載InstanceComponent
              return !GameObject.hasInstanceComponent(gameObjectState)
          })
      
          superHeroGameObjects.forEach(gameObjectState => {
              console.log("OneByOne渲染 SuperHero...")
          })
      }
      
      export let renderInstances = (worldState: worldState): void => {
          let normalHeroGameObejcts = worldState.gameObjects.filter(gameObjectState => {
              //判斷gameObject是不是掛載了InstanceComponent
              return GameObject.hasInstanceComponent(gameObjectState)
          })
      
          console.log("批量Instance渲染 NormalHeroes...")
      }
      

      renderOneByOne函數(shù)實(shí)現(xiàn)了超級(jí)英雄的渲染,它首先得到了所有沒有掛載InstanceComponent組件的gameObject;最后遍歷它們,一個(gè)一個(gè)地渲染

      renderInstances函數(shù)實(shí)現(xiàn)了普通英雄的渲染,它首先得到了所有掛載了InstanceComponent組件的gameObject;最后批量渲染

      運(yùn)行Client的代碼

      下面,我們運(yùn)行Client的代碼,打印的結(jié)果如下:

      初始化...
      更新PositionComponent
      更新PositionComponent
      更新PositionComponent
      更新PositionComponent
      OneByOne渲染 SuperHero...
      OneByOne渲染 SuperHero...
      批量Instance渲染 NormalHeroes...
      {"gameObjects":{"304480":{"positionComponent":{"gameObject":304480,"position":[0,0,0]},"velocityComponent":{"gameObject":304480,"velocity":1},"flyComponent":{"gameObject":304480,"maxVelocity":10},"instanceComponent":null},"666533":{"positionComponent":{"gameObject":666533,"position":[2,2,2]},"velocityComponent":{"gameObject":666533,"velocity":1},"flyComponent":null,"instanceComponent":{"gameObject":666533}},"838392":{"positionComponent":{"gameObject":838392,"position":[0,0,0]},"velocityComponent":{"gameObject":838392,"velocity":1},"flyComponent":null,"instanceComponent":{"gameObject":838392}},"936933":{"positionComponent":{"gameObject":936933,"position":[6,6,6]},"velocityComponent":{"gameObject":936933,"velocity":1},"flyComponent":{"gameObject":936933,"maxVelocity":10},"instanceComponent":null}}}
      

      通過打印的數(shù)據(jù),可以看到運(yùn)行的步驟與之前一樣
      不同之處在于:

      • 更新4個(gè)英雄現(xiàn)在變?yōu)楦?個(gè)positionComponent
      • 打印的WorldState不一樣

      我們看下打印的WorldState:

      • WorldState的gameObjects包括了4個(gè)gameObject的數(shù)據(jù),其中有一個(gè)gameObject數(shù)據(jù)的positionComponent的position為[2,2,2],說明它進(jìn)行了移動(dòng)操作;
      • 有一個(gè)gameObject數(shù)據(jù)的positionComponent的position為[6,6,6],說明它進(jìn)行了移動(dòng)和飛行操作

      值得注意的是:
      因?yàn)閃orldState的gameObjects中的Key是隨機(jī)生成的id值,所以每次打印時(shí)Key都不一樣

      提出問題

      • 組件的數(shù)據(jù)分散在各個(gè)組件中,性能不好
        如position數(shù)據(jù)現(xiàn)在是一對(duì)一地分散保存在各個(gè)positionComponent組件中(即一個(gè)positionComponent組件保存自己的position),那么如果需要遍歷所有組件的position數(shù)據(jù),則需要遍歷所有的positionComponent組件,分別獲得它們的position。因?yàn)槊總€(gè)positionComponent組件的數(shù)據(jù)并沒有連續(xù)地保存在內(nèi)存中,所以會(huì)造成緩存命中丟失,帶來性能損失
      • 涉及多種組件的行為不知道放在哪里
        如果超級(jí)英雄增加一個(gè)“跳”的行為,該行為不僅需要修改position數(shù)據(jù),還需要修改velocity數(shù)據(jù),那么實(shí)現(xiàn)該行為的jump函數(shù)應(yīng)該放在哪個(gè)組件中呢?
        因?yàn)閖ump函數(shù)需要同時(shí)修改PositionComponent組件的position數(shù)據(jù)和VelocityComponent組件的velocity數(shù)據(jù),所以將它放在兩者中任何一種組件中都不合適。因此需要增加一種新的組件-JumpComponent,對(duì)應(yīng)“跳”這個(gè)行為,并實(shí)現(xiàn)jump函數(shù)。該函數(shù)會(huì)通過JumpComponent掛載到的gameObject來獲得掛載到它上的PositionComponent和VelocityComponent組件,從而修改它們的數(shù)據(jù)。
        如果增加更多的這種涉及多種組件的行為,就需要為每個(gè)這樣的行為增加一種組件。因?yàn)榻M件比較重,既有數(shù)據(jù)又有邏輯,所以增加組件的成本較高;另外,因?yàn)榻M件與GameObject是聚合關(guān)系,而GameObject和World也是聚合關(guān)系,它們都屬于強(qiáng)關(guān)聯(lián)關(guān)系,所以增加組件會(huì)較強(qiáng)地影響GameObject和World,這也增加了成本

      使用ECS模式來改進(jìn)

      概述解決方案

      • 基于Data Oriented的思想進(jìn)行改進(jìn)
        組件可以按角色分為Data Oriented組件和其它組件,其中前者的特點(diǎn)是屬于該角色的每個(gè)組件都有數(shù)據(jù),且組件的數(shù)量較多;后者的特點(diǎn)是屬于該角色的每個(gè)組件都沒有數(shù)據(jù),或者組件的數(shù)量很少。這里具體說明一下各種組件的角色:目前一共有四種組件,它們是PositionComponent、VelocityComponent、FlyComponent、InstanceComponent。其中,InstanceComponent組件因?yàn)闆]有組件數(shù)據(jù),所以屬于“其它組件”;另外三種組件則都屬于“Data Oriented組件”。
        屬于Data Oriented組件的三種組件的所有組件數(shù)據(jù)將會(huì)分別集中起來,保存在各自的一塊連續(xù)的地址空間中,具體就是分別保存在三個(gè)ArrayBuffer中

      • 將GameObject和各個(gè)組件扁平化
        GameObject不再有數(shù)據(jù)和邏輯了,而只是一個(gè)全局唯一的id。組件也不再有數(shù)據(jù)和邏輯了,其中屬于“Data Oriented組件”的組件只是一個(gè)ArrayBuffer上的索引;屬于“其它組件”的組件只是一個(gè)全局唯一的id

      • 增加Component+GameObject這一層,將扁平的GameObject和組件放在該層中

      • 增加Manager這一層,來管理GameObject和組件的數(shù)據(jù)
        這一層有GameObjectManager和四種組件的Manager,其中GameObjectManager負(fù)責(zé)管理所有的gameObject;四種組件的Manager負(fù)責(zé)管理自己的ArrayBuffer,操作屬于該種類的所有組件

      • 增加System這一層,來實(shí)現(xiàn)行為的邏輯
        一個(gè)System實(shí)現(xiàn)一個(gè)行為,比如這一層中的MoveSystem、FlySystem分別實(shí)現(xiàn)了移動(dòng)和飛行的行為邏輯

      值得注意的是:

      • GameObject和組件的數(shù)據(jù)被移到了Manager中,邏輯則被移到了Manager和System中。其中只操作自己數(shù)據(jù)的邏輯(如getPosition、setPosition)被移到了Manager中,其它邏輯(通常為行為邏輯,需要操作多種組件)被移到了System中
      • 一種組件的Manager只對(duì)該種組件進(jìn)行操作,而一個(gè)System可以對(duì)多種組件進(jìn)行操作

      給出UML

      領(lǐng)域模型

      image

      總體來看,領(lǐng)域模型分為五個(gè)部分:用戶、World、System層、Manager層、Component+GameObject層,它們的依賴關(guān)系是前者依賴后者

      我們看下用戶、World這兩個(gè)部分:
      Client是用戶

      World是游戲世界,雖然仍然實(shí)現(xiàn)了初始化和主循環(huán)的邏輯,不過不再管理所有的GameObject了

      我們看下System這一層:

      有多個(gè)System,每個(gè)System實(shí)現(xiàn)一個(gè)行為邏輯。每個(gè)System的職責(zé)如下:

      • CreateStateSystem實(shí)現(xiàn)創(chuàng)建WorldState的邏輯,創(chuàng)建的WorldState包括了所有的Manager的state數(shù)據(jù);
      • UpdateSystem實(shí)現(xiàn)更新所有人物的position的邏輯,具體是更新所有PositionComponent的position;
      • MoveSystem實(shí)現(xiàn)一個(gè)人物的移動(dòng)的邏輯,具體是根據(jù)掛載到該人物gameObject上的一個(gè)positionComponent和一個(gè)velocityComponent,更新該positionComponent的position;
      • FlySystem實(shí)現(xiàn)一個(gè)人物的飛行的邏輯,具體是根據(jù)掛載到該人物gameObject上的一個(gè)positionComponent、一個(gè)velocityComponent、一個(gè)flyComponent,更新該positionComponent的position;
      • RenderOneByOneSystem實(shí)現(xiàn)渲染所有超級(jí)英雄的邏輯;
      • RenderInstancesSystem實(shí)現(xiàn)渲染所有普通英雄的邏輯

      我們看下Manager這一層:

      每個(gè)Manager都有一個(gè)state數(shù)據(jù)

      GameObjectManager負(fù)責(zé)管理所有的gameObject

      PositionComponentManager、VelocityComponentManager、FlyComponentManager、InstanceComponentManager負(fù)責(zé)管理屬于各自種類的所有的組件

      PositionComponentManager的state數(shù)據(jù)包括一個(gè)buffer字段和一個(gè)positions字段,其中前者是一個(gè)ArrayBuffer,保存了該種組件的所有組件數(shù)據(jù);后者是buffer的視圖,用于讀寫buffer中的position數(shù)據(jù)

      PositionComponentManager的batchUpdate函數(shù)負(fù)責(zé)批量更新所有的positionComponent組件的position

      因?yàn)閂elocityComponentManager、FlyComponentManager與PositionComponentManager一樣,都屬于Data Oriented組件的Manager,所以它們的數(shù)據(jù)和函數(shù)類似(只是沒有batchUpdate函數(shù)),故在圖中省略它們的數(shù)據(jù)和函數(shù)

      我們看下Component+GameObject這一層:

      因?yàn)镻ositionComponent、VelocityComponent、FlyComponent屬于Data Oriented組件,所以它們是一個(gè)index,也就是各自Manager的state的buffer中的索引值

      因?yàn)镮nstanceComponent屬于其它組件,所以它是一個(gè)全局唯一的id

      GameObject是一個(gè)全局唯一的id

      我們來看下依賴關(guān)系:

      System層:

      因?yàn)镃reateSystem需要調(diào)用各個(gè)Manager的createState函數(shù)來創(chuàng)建它們的state,所以依賴了整個(gè)Manager層

      因?yàn)閁pdateSystem需要調(diào)用PositionComponentManager的batchUpdate函數(shù)來更新,所以依賴了PositionComponentManager

      因?yàn)镸oveSystem需要調(diào)用PositionComponentManager來獲得和設(shè)置position,并且調(diào)用VelocityComponentManager來獲得velocity,所以依賴了PositionComponentManager、VelocityComponentManager

      因?yàn)镕lySystem需要調(diào)用PositionComponentManager來獲得和設(shè)置position,并且調(diào)用VelocityComponentManager、FlyComponentManager來分別獲得velocity和maxVelocity,所以依賴了PositionComponentManager、VelocityComponentManager、FlyComponentManager

      因?yàn)镽enderOneByOneSystem和RenderInstancesSystem需要調(diào)用GameObjectManager來獲得所有的gameObject,并調(diào)用各種組件的Manager來獲得組件數(shù)據(jù),所以依賴了整個(gè)Manager層

      Manager層:

      因?yàn)镚ameObjectManager需要操作GameObject,所以依賴了GameObject

      因?yàn)楦鞣N組件的Manager需要操作所有的該種組件,所以依賴了對(duì)應(yīng)的組件

      結(jié)合UML圖,描述如何具體地解決問題

      • 現(xiàn)在各種組件的數(shù)據(jù)都集中保存在各自Manager的state的buffer(ArrayBuffer)中,遍歷同一種組件的所有組件數(shù)據(jù)即是遍歷一個(gè)ArrayBuffer。因?yàn)锳rrayBuffer的數(shù)據(jù)是連續(xù)地保存在內(nèi)存中的,所以緩存命中不會(huì)丟失,從而提高了性能

      • 現(xiàn)在將涉及多種組件的行為放在對(duì)應(yīng)的System中。因?yàn)镾ystem很輕,沒有數(shù)據(jù),只有邏輯,所以增加和維護(hù)System的成本較低;另外,因?yàn)镾ystem位于最上層,所以修改System也不會(huì)影響Manager層和Component+GameObject層

      給出代碼

      首先,我們看下Client的代碼;
      然后,我們看下Client代碼中第一步的代碼:

      • 創(chuàng)建WorldState的代碼

      然后,因?yàn)閯?chuàng)建WorldState時(shí)會(huì)創(chuàng)建Data Oriented組件的Manager的state,其中的關(guān)健是創(chuàng)建各自的ArrayBuffer,所以我們看下創(chuàng)建它的代碼;
      然后,我們看下Client代碼中第二步的代碼:

      • 創(chuàng)建場(chǎng)景的代碼

      然后,因?yàn)閯?chuàng)建場(chǎng)景時(shí)操作了普通英雄和超級(jí)英雄,所以我們看下它們的代碼,它們包括:

      • 移動(dòng)的相關(guān)代碼
      • 飛行的相關(guān)代碼

      然后,我們依次看下Client代碼中剩余的兩個(gè)步驟的代碼,它們包括:

      • 初始化和主循環(huán)的代碼

      然后,我們看下主循環(huán)的一幀中每個(gè)步驟的代碼,它們包括:

      • 主循環(huán)中更新的代碼
      • 主循環(huán)中渲染的代碼

      最后,我們運(yùn)行Client的代碼

      Client的代碼

      Client

      let worldState = World.createState({ positionComponentCount: 10, velocityComponentCount: 10, flyComponentCount: 10 })
      
      跟之前一樣...
      

      Client的代碼跟之前的Client的代碼基本一樣,除了createState函數(shù)的參數(shù)和_createScene函數(shù)中創(chuàng)建場(chǎng)景的方式不一樣,這個(gè)等會(huì)再討論

      創(chuàng)建WorldState的代碼

      World

      export let createState = CreateStateSystem.createState
      

      CreateStateSystem

      export let createState = ({ positionComponentCount, velocityComponentCount, flyComponentCount }): worldState => {
          return {
              gameObjectManagerState: GameObjectManager.createState(),
              positionComponentManagerState: PositionComponentManager.createState(positionComponentCount),
              velocityComponentManagerState: VelocityComponentManager.createState(velocityComponentCount),
              flyComponentManagerState: FlyComponentManager.createState(flyComponentCount),
              instanceComponentManagerState: InstanceComponentManager.createState()
          }
      }
      

      CreateStateSystem的createState函數(shù)創(chuàng)建了WorldState,它保存了各個(gè)Manager的state

      因?yàn)镈ata Oriented組件的Manager的state在創(chuàng)建時(shí)要?jiǎng)?chuàng)建包括該種組件的所有組件數(shù)據(jù)的ArrayBuffer,需要知道該種組件的最大個(gè)數(shù),所以這里的createState函數(shù)接收了三種Data Oriented組件的最大個(gè)數(shù)

      創(chuàng)建ArrayBuffer的代碼

      我們以PositionComponentManager為例,來看下它的createState函數(shù)的相關(guān)代碼:
      position_component/ManagerStateType

      export type state = {
          maxIndex: number,
          buffer: ArrayBuffer,
          positions: Float32Array,
          ...
      }
      

      這是PositionComponentManager的state的類型定義,它的字段解釋如下:

      • buffer字段保存了一個(gè)ArrayBuffer,它用來保存所有的positionComponent的數(shù)據(jù)。目前每個(gè)positionComponent的數(shù)據(jù)只有position,它的類型是三個(gè)float
      • positions字段保存了ArrayBuffer的一個(gè)視圖,通過它可以讀寫所有的positionComponent的position
      • maxIndex字段是ArrayBuffer上最大的索引值,用于在創(chuàng)建一個(gè)positionComponent時(shí)生成它的index值

      position_component/Manager

      let _setAllTypeArrDataToDefault = ([positions]: Array<Float32Array>, count, [defaultPosition]) => {
          range(0, count - 1).forEach(index => {
              OperateTypeArrayUtils.setPosition(index, defaultPosition, positions)
          })
      
          return [positions]
      }
      
      let _initBufferData = (count, defaultDataTuple): [ArrayBuffer, Array<Float32Array>] => {
          let buffer = BufferUtils.createBuffer(count)
      
          let typeArrData = _setAllTypeArrDataToDefault(CreateTypeArrayUtils.createTypeArrays(buffer, count), count, defaultDataTuple)
      
          return [buffer, typeArrData]
      }
      
      export let createState = (positionComponentCount: number): state => {
          let defaultPosition = [0, 0, 0]
      
          let [buffer, [positions]] = _initBufferData(positionComponentCount, [defaultPosition])
      
          return {
              maxIndex: 0,
              buffer,
              positions,
              ...
          }
      }
      

      這是PositionComponentManager的createState函數(shù)的代碼,其中調(diào)用的_initBufferData函數(shù)創(chuàng)建了buffer和positions,它的步驟如下:
      1.調(diào)用BufferUtils的createBuffer函數(shù)來創(chuàng)建包括最大組件個(gè)數(shù)的數(shù)據(jù)的ArrayBuffer
      2.調(diào)用CreateTypeArrayUtils的createTypeArrays函數(shù)來創(chuàng)建所有的TypeArray,它們是操作ArrayBuffer的視圖。這里具體是只創(chuàng)建了一個(gè)視圖:positions
      3.調(diào)用_setAllTypeArrDataToDefault函數(shù)來將positions的所有的值寫為默認(rèn)值:[0,0,0]

      下面是BufferUtils的createBuffer函數(shù)和CreateTypeArrayUtils的createTypeArrays函數(shù)的相關(guān)代碼:
      position_component/BufferUtils

      let _getPositionSize = () => 3
      
      export let getPositionOffset = (count) => 0
      
      export let getPositionLength = (count) => count * _getPositionSize()
      
      export let getPositionIndex = index => index * _getPositionSize()
      
      let _getTotalByteLength = (count) => {
          return count * Float32Array.BYTES_PER_ELEMENT * _getPositionSize()
      }
      
      export let createBuffer = (count) => {
          return new ArrayBuffer(_getTotalByteLength(count))
      }
      

      position_component/CreateTypeArrayUtils

      export let createTypeArrays = (buffer, count) => {
          return [
              new Float32Array(buffer, BufferUtils.getPositionOffset(count), BufferUtils.getPositionLength(count))
          ]
      }
      

      另外兩種Data Oriented組件的Manager(VelocityComponentManager、FlyComponentManager)的createState函數(shù)的邏輯跟PositionComponentManager的createState函數(shù)的邏輯一樣,故省略相關(guān)代碼

      創(chuàng)建場(chǎng)景的代碼

      Client

      let _createScene = (worldState: worldState): worldState => {
          創(chuàng)建normalHero1:
              創(chuàng)建gameObject
              創(chuàng)建positionComponent
              創(chuàng)建velocityComponent
              創(chuàng)建instanceComponent
              掛載positionComponent、velocityComponent、instanceComponent到gameObject
      
          創(chuàng)建normalHero2
      
          normalHero1移動(dòng):
              調(diào)用MoveSystem的move函數(shù),傳入normalHero1的positionComponent、velocityComponent
      
          創(chuàng)建superHero1
              創(chuàng)建gameObject
              創(chuàng)建positionComponent
              創(chuàng)建velocityComponent
              創(chuàng)建flyComponent
              掛載positionComponent、velocityComponent、flyComponent到gameObject
      
          創(chuàng)建superHero2
      
          superHero1移動(dòng):
              調(diào)用MoveSystem的move函數(shù),傳入superHero1的positionComponent、velocityComponent
          superHero1飛行:
              調(diào)用FlySystem的fly函數(shù),傳入superHero1的positionComponent、velocityComponent、flyComponent
      
          return worldState
      }
      

      _createScene函數(shù)創(chuàng)建了場(chǎng)景,場(chǎng)景的內(nèi)容跟之前一樣,都包括了2個(gè)普通英雄和2個(gè)超級(jí)英雄,只是現(xiàn)在創(chuàng)建一個(gè)英雄的方式又改變了,具體變?yōu)椋含F(xiàn)在不需要加入GameObject到World中

      另外,現(xiàn)在改為通過調(diào)用MoveSystem和FlySystem的函數(shù)來操作對(duì)應(yīng)的組件,從而實(shí)現(xiàn)英雄的“移動(dòng)”、“飛行”

      gameObject/Manager

      export let createState = (): state => {
          return {
              maxUID: 0
          }
      }
      
      //創(chuàng)建一個(gè)gameObject
      //一個(gè)gameObject就是一個(gè)uid
      export let createGameObject = (state: state): [state, gameObject] => {
          let uid = state.maxUID
      
          //生成一個(gè)uid
          //uid的意思是unique id,即全局唯一的id
          let newUID = uid + 1
      
          state = {
              ...state,
              maxUID: newUID
          }
      
          return [state, uid]
      }
      

      GameObjectManager的createGameObject函數(shù)創(chuàng)建了一個(gè)gameObject,它就是一個(gè)全局唯一的id

      position_component/Manager

      //創(chuàng)建一個(gè)positionComponent
      //一個(gè)positionComponent就是一個(gè)index
      export let createComponent = (state: state): [state, component] => {
          let index = state.maxIndex
              
          //生成一個(gè)index
          let newIndex = index + 1
      
          state = {
              ...state,
              maxIndex: newIndex
          }
      
          return [state, index]
      }
      

      PositionComponentManager的createComponent函數(shù)創(chuàng)建了一個(gè)positionComponent,它就是一個(gè)PositionComponentManager的state的buffer上的索引

      VelocityComponentManager、FlyComponentManager的相關(guān)代碼跟PositionComponentManager類似,故省略相關(guān)代碼

      instance_component/Manager

      //創(chuàng)建一個(gè)instanceComponent
      //一個(gè)instanceComponent就是一個(gè)uid
      export let createComponent = (state: state): [state, component] => {
          let uid = state.maxUID
      
          //生成一個(gè)id
          let newUID = uid + 1
      
          state = {
              ...state,
              maxUID: newUID
          }
      
          return [state, uid]
      }
      

      InstanceComponentManager的createComponent函數(shù)創(chuàng)建了一個(gè)instanceComponent,因?yàn)镮nstanceComponent組件屬于“其它組件”,所以它跟GameObject一樣都是一個(gè)全局唯一的id而不是一個(gè)index

      移動(dòng)的相關(guān)代碼

      MoveSystem

      //一個(gè)gameObject的移動(dòng)
      export let move = (worldState: worldState, positionComponent, velocityComponent): worldState => {
          let [x, y, z] = PositionComponentManager.getPosition(worldState.positionComponentManagerState, positionComponent)
      
          let velocity = VelocityComponentManager.getVelocity(worldState.velocityComponentManagerState, velocityComponent)
      
          //根據(jù)velocity,更新positionComponent的position
          let positionComponentManagerState = PositionComponentManager.setPosition(worldState.positionComponentManagerState, positionComponent, [x + velocity, y + velocity, z + velocity])
      
          return {
              ...worldState,
              positionComponentManagerState: positionComponentManagerState
          }
      }
      

      MoveSystem的move函數(shù)實(shí)現(xiàn)移動(dòng)的行為邏輯。這里涉及到讀寫Data Oriented組件的ArrayBuffer上的數(shù)據(jù)。我們來看下讀寫PositionComponentManager的positions的相關(guān)代碼:
      position_component/Manager

      export let getPosition = (state: state, component: component) => {
          return OperateTypeArrayUtils.getPosition(component, state.positions)
      }
      
      export let setPosition = (state: state, component: component, position) => {
          OperateTypeArrayUtils.setPosition(component, position, state.positions)
      
          return state
      }
      

      position_component/OperateTypeArrayUtils

      export let getPosition = (index, typeArr) => {
          return TypeArrayUtils.getFloat3Tuple(BufferUtils.getPositionIndex(index), typeArr)
      }
      
      export let setPosition = (index, data, typeArr) => {
          TypeArrayUtils.setFloat3(BufferUtils.getPositionIndex(index), data, typeArr)
      }
      

      TypeArrayUtils

      export let getFloat3Tuple = (index, typeArray) => {
          return [
              typeArray[index],
              typeArray[index + 1],
              typeArray[index + 2]
          ]
      }
      
      export let setFloat3 = (index, param, typeArray) => {
          typeArray[index] = param[0]
          typeArray[index + 1] = param[1]
          typeArray[index + 2] = param[2]
      }
      

      position_component/BufferUtils

      let _getPositionSize = () => 3
      
      ...
      
      export let getPositionIndex = index => index * _getPositionSize()
      

      通過代碼可知,實(shí)現(xiàn)“讀寫PositionComponentManager的ArrayBuffer上的數(shù)據(jù)”的思路是:
      因?yàn)橐粋€(gè)positionComponent的值是ArrayBuffer的索引,所以使用它來讀寫ArrayBuffer的視圖positions中的對(duì)應(yīng)數(shù)據(jù)

      飛行的相關(guān)代碼

      FlySystem

      //一個(gè)gameObject的飛行
      export let fly = (worldState: worldState, positionComponent, velocityComponent, flyComponent): worldState => {
          let [x, y, z] = PositionComponentManager.getPosition(worldState.positionComponentManagerState, positionComponent)
      
          let velocity = VelocityComponentManager.getVelocity(worldState.velocityComponentManagerState, velocityComponent)
      
          let maxVelocity = FlyComponentManager.getMaxVelocity(worldState.flyComponentManagerState, flyComponent)
      
          //根據(jù)maxVelociy、velocity,更新positionComponent的position
          velocity = velocity < maxVelocity ? (velocity * 2.0) : maxVelocity
          let positionComponentManagerState = PositionComponentManager.setPosition(worldState.positionComponentManagerState, positionComponent, [x + velocity, y + velocity, z + velocity])
      
          return {
              ...worldState,
              positionComponentManagerState: positionComponentManagerState
          }
      
      }
      

      FlySystem的fly函數(shù)實(shí)現(xiàn)了飛行的行為邏輯

      初始化和主循環(huán)的代碼

      初始化和主循環(huán)的邏輯跟之前一樣,故省略代碼

      主循環(huán)中更新的代碼

      World

      export let update = UpdateSystem.update
      

      UpdateSystem

      export let update = (worldState: worldState): worldState => {
          let positionComponentManagerState = PositionComponentManager.batchUpdate(worldState.positionComponentManagerState)
      
          return {
              ...worldState,
              positionComponentManagerState: positionComponentManagerState
          }
      }
      

      UpdateSystem的update函數(shù)實(shí)現(xiàn)了更新,它調(diào)用了PositionComponentManager的batchUpdate函數(shù)來批量更新所有的positionComponent組件。我們看下PositionComponentManager的相關(guān)代碼:
      position_component/Manager

      export let getAllComponents = (state: state): Array<component> => {
          從state中獲得所有的positionComponents
      }
      
      export let batchUpdate = (state: state) => {
          return getAllComponents(state).reduce((state, component) => {
              更新position
          }, state)
      }
      

      batchUpdate函數(shù)遍歷所有的positionComponent,更新它們的position。更新的邏輯跟之前一樣

      主循環(huán)中渲染的代碼

      World

      export let renderOneByOne = RenderOneByOneSystem.render
      

      RenderOneByOneSystem

      export let render = (worldState: worldState): void => {
          let superHeroGameObjects = GameObjectManager.getAllGameObjects(worldState.gameObjectManagerState).filter(gameObject => {
              //判斷gameObject是不是沒有掛載InstanceComponent
              return !InstanceComponentManager.hasComponent(worldState.instanceComponentManagerState, gameObject)
          })
      
          superHeroGameObjects.forEach(gameObjectState => {
              console.log("OneByOne渲染 SuperHero...")
          })
      }
      

      gameObject/Manager

      export let getAllGameObjects = (state: state): Array<gameObject> => {
          let { maxUID } = state
      
          //返回[0, 1, ...,  maxUID-1]的數(shù)組
          return range(0, maxUID - 1)
      }
      

      RenderOneByOneSystem的render函數(shù)實(shí)現(xiàn)了超級(jí)英雄的渲染,它首先得到了所有沒有掛載InstanceComponent組件的gameObject;最后一個(gè)一個(gè)地渲染

      World

      export let renderInstances = RenderInstancesSystem.render
      

      RenderInstancesSystem

      export let render = (worldState: worldState): void => {
          let normalHeroGameObejcts = GameObjectManager.getAllGameObjects(worldState.gameObjectManagerState).filter(gameObject => {
              //判斷gameObject是不是掛載了InstanceComponent
              return InstanceComponentManager.hasComponent(worldState.instanceComponentManagerState, gameObject)
          })
      
          console.log("批量Instance渲染 NormalHeroes...")
      }
      

      RenderInstancesSystem的render函數(shù)實(shí)現(xiàn)了普通英雄的渲染,它首先得到了所有掛載了InstanceComponent組件的gameObject;最后一次性批量渲染

      運(yùn)行Client的代碼

      下面,我們運(yùn)行Client的代碼,打印的結(jié)果如下:

      初始化...
      更新PositionComponent: 0
      更新PositionComponent: 1
      更新PositionComponent: 2
      更新PositionComponent: 3
      OneByOne渲染 SuperHero...
      OneByOne渲染 SuperHero...
      批量Instance渲染 NormalHeroes...
      {"gameObjectManagerState":{"maxUID":4},"positionComponentManagerState":{"maxIndex":4,"buffer":{},"positions":{"0":2,"1":2,"2":2,"3":0,"4":0,"5":0,"6":6,"7":6,"8":6,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0},"gameObjectMap":{"0":0,"1":1,"2":2,"3":3},"gameObjectPositionMap":{"0":0,"1":1,"2":2,"3":3}},"velocityComponentManagerState":{"maxIndex":4,"buffer":{},"velocitys":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1},"gameObjectMap":{"0":0,"1":1,"2":2,"3":3},"gameObjectVelocityMap":{"0":0,"1":1,"2":2,"3":3}},"flyComponentManagerState":{"maxIndex":1,"buffer":{},"maxVelocitys":{"0":10,"1":10,"2":10,"3":10,"4":10,"5":10,"6":10,"7":10,"8":10,"9":10},"gameObjectMap":{"0":2,"1":3},"gameObjectFlyMap":{"2":0,"3":1}},"instanceComponentManagerState":{"maxUID":1,"gameObjectMap":{"0":0,"1":1},"gameObjectInstanceMap":{"0":0,"1":1}}}
      

      通過打印的數(shù)據(jù),可以看到運(yùn)行的步驟與之前一樣
      不同之處在于:

      • 打印的WorldState不一樣

      我們看下打印的WorldState:

      • WorldState的gameObjectManagetState的maxUID為4,說明創(chuàng)建了4個(gè)gameObject;
      • WorldState的positionComponentManagerState的maxIndex為4,說明創(chuàng)建了4個(gè)positionComponent;
      • WorldState的positionComponentManagerState的positions有3個(gè)連續(xù)的值是2、2、2,說明有一個(gè)positionComponent組件進(jìn)行了移動(dòng)操作;有另外3個(gè)連續(xù)的值是6、6、6,說明有另外一個(gè)positionComponent組件進(jìn)行了移動(dòng)操作和飛行操作;

      定義

      一句話定義

      組合代替繼承;連續(xù)地保存組件數(shù)據(jù);分離邏輯和數(shù)據(jù)

      補(bǔ)充說明

      “組合代替繼承”是指基于組件化思想,通過GameObject組合不同的組件代替GameObject層層繼承

      “連續(xù)地保存組件數(shù)據(jù)”是指基于Data Oriented思想,將Data Oriented組件的組件數(shù)據(jù)集中起來,保存在內(nèi)存中連續(xù)的地址空間

      “分離邏輯和數(shù)據(jù)”是指將GameObject和組件扁平化,將它們的數(shù)據(jù)放到Manager層,將它們的邏輯放到System層和Manager層。其中,只操作自己數(shù)據(jù)的邏輯(如getPosition、setPosition)被移到了Manager中,其它邏輯(通常為行為邏輯,需要操作多種組件)被移到了System中

      通用UML

      領(lǐng)域模型

      image

      總體來看,領(lǐng)域模型分為用戶、World、System層、Manager層、Component+GameObject層五個(gè)部分,它們的依賴關(guān)系是前者依賴后者。其中,System層負(fù)責(zé)實(shí)現(xiàn)行為的邏輯;Manager層負(fù)責(zé)管理場(chǎng)景數(shù)據(jù),即管理GameObject和組件的數(shù)據(jù);Component+GameObject層為組件和GameObject,它們現(xiàn)在只是有一個(gè)number類型的數(shù)據(jù)的值對(duì)象

      我們看下用戶這個(gè)部分:

      • Client
        該角色是用戶

      我們看下World這個(gè)部分:

      • World
        該角色是門戶,提供了API,實(shí)現(xiàn)了初始化和主循環(huán)的邏輯

      我們看下System這一層:

      一個(gè)System實(shí)現(xiàn)一個(gè)行為的邏輯

      • CreateStateSystem
        該角色負(fù)責(zé)創(chuàng)建WorldState

      • OtherSystem
        該角色是除了CreateStateSystem以外的所有System,它們各自有一個(gè)函數(shù)action,用于實(shí)現(xiàn)某個(gè)行為

      我們看下Manager這一層:

      每個(gè)Manager都有一個(gè)state數(shù)據(jù)

      • GameObjectManager
        該角色負(fù)責(zé)管理所有的gameObject

      • DataOrientedComponentManager
        該角色是一種Data Oriented組件的Manager,負(fù)責(zé)維護(hù)和管理該種組件的所有組件數(shù)據(jù),將其集中地保存在各自的state的buffer中
        DataOrientedComponentManager的state數(shù)據(jù)包括一個(gè)buffer字段和多個(gè)“typeArray of buffer”字段,其中前者是一個(gè)ArrayBuffer,保存了該種組件的所有組件數(shù)據(jù);后者是buffer的多個(gè)視圖,用于讀寫buffer中對(duì)應(yīng)的數(shù)據(jù)

      • OtherComponentManager
        該角色是一種其它組件的Manager,負(fù)責(zé)維護(hù)和管理該種組件的所有組件數(shù)據(jù)
        OtherComponentManager的state數(shù)據(jù)包括多個(gè)“value map”字段,它們是多個(gè)Hash Map,每個(gè)Hash Map保存一類組件數(shù)據(jù)

      我們看下Component+GameObject這一層:

      • DataOrientedComponent
        該角色是一種Data Oriented組件中的一個(gè)組件,它是一個(gè)ArrayBuffer上的索引

      • OtherComponent
        該角色是一種其它組件中的一個(gè)組件,它是一個(gè)全局唯一的id

      • GameObject
        該角色是一個(gè)gameObject,它是一個(gè)全局唯一的id

      角色之間的關(guān)系

      • 只有一個(gè)CreateStateSystem

      • 可以有多個(gè)OtherSystem,每個(gè)OtherSystem實(shí)現(xiàn)一個(gè)行為

      • 只有一個(gè)GameObjectManager

      • 可以有多個(gè)DataOrientedComponentManager,每個(gè)對(duì)應(yīng)一種Data Oriented組件

      • 可以有多個(gè)DataOrientedComponent,每個(gè)對(duì)應(yīng)一種Data Oriented組件

      • 可以有多個(gè)OtherComponentManager,每個(gè)對(duì)應(yīng)一種其它組件

      • 可以有多個(gè)OtherComponent,每個(gè)對(duì)應(yīng)一種其它組件

      System層:

      • 因?yàn)镃reateSystem需要調(diào)用各個(gè)Manager的createState函數(shù)來創(chuàng)建它們的state,所以依賴了整個(gè)Manager層
      • 因?yàn)镺therSystem可能需要調(diào)用1個(gè)GameObjectManager來處理gameObject、調(diào)用多個(gè)DataOrientedComponentManager和OtherComponentManager來處理各自種類的組件,所以它與GameObjectManager是一對(duì)一的依賴關(guān)系,與DataOrientedComponentManager和OtherComponentManager都是一對(duì)多的依賴關(guān)系

      Manager層:

      • 因?yàn)镚ameObjectManager需要操作多個(gè)GameObject,所以它與GameObject是一對(duì)多的依賴關(guān)系
      • 因?yàn)镈ataOrientedComponentManager和DataOrientedComponent對(duì)應(yīng)同一種Data Oriented組件,且前者管理所有的后者,所以前者和后者是一對(duì)多的依賴關(guān)系

      • 因?yàn)镺therComponentManager和OtherComponent對(duì)應(yīng)同一種其它組件,且前者管理所有的后者,所以前者和后者是一對(duì)多的依賴關(guān)系

      Component+GameObject層:

      • 因?yàn)橐粋€(gè)GameObject可以掛載各種組件,其中每種組件只能掛載一個(gè),所以GameObject與DataOrientedComponent、OtherComponent都是一對(duì)一的組合關(guān)系

      角色的抽象代碼

      下面我們來看看各個(gè)角色的抽象代碼:

      我們按照依賴關(guān)系,從上往下依次看下領(lǐng)域模型中用戶、World、System層、Manager層、Component+GameObject層這五個(gè)部分的抽象代碼:

      首先,我們看下屬于用戶的抽象代碼
      然后,我們看下World的抽象代碼
      然后,我們看下System層的抽象代碼,它們包括:

      • CreateStateSystem的抽象代碼
      • OtherSystem的抽象代碼

      然后,我們看下Manager層的抽象代碼,它們包括:

      • GameObjectManager的抽象代碼
      • DataOrientedComponentManager的抽象代碼
      • OtherComponentManager的抽象代碼

      最后,我們看下Component+GameObject層的抽象代碼,它們包括:

      • GameObject的抽象代碼
      • DataOrientedComponent的抽象代碼
      • OtherComponent的抽象代碼

      用戶的抽象代碼

      Client

      let _createScene = (worldState: worldState): worldState => {
          創(chuàng)建gameObject1
          創(chuàng)建組件
          掛載組件
      
          觸發(fā)gameObject1的行為
      
          創(chuàng)建更多的gameObjects...
      
          return worldState
      }
      
      let worldState = World.createState({ dataOrientedComponent1Count: xx })
      
      worldState = _createScene(worldState)
      
      worldState = World.init(worldState)
      
      World.loop(worldState)
      

      World的抽象代碼

      World

      export let createState = CreateStateSystem.createState
      
      export let action1 = OtherSystem1.action
      
      export let init = (worldState: worldState): worldState => {
          初始化...
      
          return worldState
      }
      
      
      //假實(shí)現(xiàn)
      let requestAnimationFrame = (func) => {
      }
      
      
      export let loop = (worldState: worldState) => {
          調(diào)用OtherSystem來更新
      
          調(diào)用OtherSystem來渲染
      
          requestAnimationFrame(
              (time) => {
                  loop(worldState)
              }
          )
      }
      

      CreateStateSystem的抽象代碼

      CreateStateSystem

      export let createState = ({ dataOrientedComponent1Count }): worldState => {
          return {
              gameObjectManagerState: GameObjectManager.createState(),
              dataOrientedComponent1ManagerState: DataOrientedComponent1Manager.createState(dataOrientedComponent1Count),
              otherComponent1ManagerState: OtherComponent1Manager.createState(),
      
              創(chuàng)建更多的DataOrientedManagerState和OtherComponentManagerState...
          }
      }
      

      OtherSystem的抽象代碼

      OtherSystem1

      export let action = (worldState: worldState, gameObject?: gameObject, dataOrientedComponentX?: dataOrientedComponentX, otherComponentX?: otherComponentX) => {
          行為的邏輯...
      
          return worldState
      }
      

      有多個(gè)OtherSystem,這里只給出一個(gè)OtherSystem的抽象代碼

      GameObjectManager的抽象代碼

      gameObject/ManagerStateType

      export type state = {
          maxUID: number
      }
      

      gameObject/Manager

      export let createState = (): state => {
          return {
              maxUID: 0
          }
      }
      
      export let createGameObject = (state: state): [state, gameObject] => {
          let uid = state.maxUID
      
          let newUID = uid + 1
      
          state = {
              ...state,
              maxUID: newUID
          }
      
          return [state, uid]
      }
      
      export let getAllGameObjects = (state: state): Array<gameObject> => {
          let { maxUID } = state
      
          return range(0, maxUID - 1)
      }
      

      DataOrientedComponentManager的抽象代碼

      dataoriented_component1/ManagerStateType

      export type TypeArrayType = Float32Array | Uint8Array | Uint16Array | Uint32Array
      
      export type state = {
          maxIndex: number,
          //buffer保存了該種組件所有的value1、value2、...、valueX數(shù)據(jù)
          buffer: ArrayBuffer,
          //該種組件所有的value1數(shù)據(jù)的視圖
          value1s: TypeArrayType,
          //該種組件所有的value2數(shù)據(jù)的視圖
          value2s: TypeArrayType,
          更多valueXs...,
      
          ...
      }
      

      dataoriented_component1/Manager

      let _setAllTypeArrDataToDefault = ([value1s, value2s]: Array<Float32Array>, count, [defaultValue1, defaultValue2]) => {
          range(0, count - 1).forEach(index => {
              OperateTypeArrayUtils.setValue1(index, defaultValue1, value1s)
              OperateTypeArrayUtils.setValue2(index, defaultValue2, value2s)
          })
      
          return [value1s, value2s]
      }
      
      let _initBufferData = (count, defaultDataTuple): [ArrayBuffer, Array<TypeArrayType>] => {
          let buffer = BufferUtils.createBuffer(count)
      
          let typeArrData = _setAllTypeArrDataToDefault(CreateTypeArrayUtils.createTypeArrays(buffer, count), count, defaultDataTuple)
      
          return [buffer, typeArrData]
      }
      
      export let createState = (dataorientedComponentCount: number): state => {
          let defaultValue1 = default value1
          let defaultValue2 = default value2
      
          let [buffer, [value1s, value2s]] = _initBufferData(dataorientedComponentCount, [defaultValue1, defaultValue2])
      
          return {
              maxIndex: 0,
              buffer,
              value1s,
              value2s,
              ...
          }
      }
      
      export let createComponent = (state: state): [state, component] => {
          let index = state.maxIndex
      
          let newIndex = index + 1
      
          state = {
              ...state,
              maxIndex: newIndex
          }
      
          return [state, index]
      }
      
      ...
      
      export let getAllComponents = (state: state): Array<component> => {
          從state中獲得所有的dataorientedComponent1s
      }
      
      export let getValue1 = (state: state, component: component) => {
          return OperateTypeArrayUtils.getValue1(component, state.value1s)
      }
      
      export let setValue1 = (state: state, component: component, position) => {
          OperateTypeArrayUtils.setValue1(component, position, state.value1s)
      
          return state
      }
      
      get/set value2...
      
      export let batchOperate = (state: state) => {
          let allComponents = getAllComponents(state)
      
          console.log("批量操作")
      
          return state
      }
      

      dataoriented_component1/BufferUtils

      // 這里只給出了兩個(gè)value的情況
      // 更多的value也以此類推...
      
      let _getValue1Size = () => value1 size
      
      let _getValue2Size = () => value2 size
      
      export let getValue1Offset = () => 0
      
      export let getValue2Offset = (count) => getValue1Offset() + getValue1Length(count) * TypeArray2.BYTES_PER_ELEMENT
      
      export let getValue1Length = (count) => count * _getValue1Size()
      
      export let getValue2Length = (count) => count * _getValue2Size()
      
      export let getValue1Index = index => index * _getValue1Size()
      
      export let getValue2Index = index => index * _getValue2Size()
      
      let _getTotalByteLength = (count) => {
          return count * (TypeArray1.BYTES_PER_ELEMENT * (_getValue1Size() + TypeArray2.BYTES_PER_ELEMENT * (_getValue2Size())))
      }
      
      export let createBuffer = (count) => {
          return new ArrayBuffer(_getTotalByteLength(count))
      }
      

      dataoriented_component1/CreateTypeArrayUtils

      export let createTypeArrays = (buffer, count) => {
          return [
              new Float32Array(buffer, BufferUtils.getValue1Offset(), BufferUtils.getValue1Length(count)),
              new Float32Array(buffer, BufferUtils.getValue2Offset(count), BufferUtils.getValue2Length(count)),
          ]
      }
      

      有多個(gè)DataOrientedComponentManager,這里只給出一個(gè)DataOrientedComponentManager的抽象代碼

      OtherComponentManager的抽象代碼

      other_component1/ManagerStateType

      export type state = {
          maxUID: number,
          //value1Map用來保存該種組件所有的value1數(shù)據(jù)
          value1Map: Map<component, value1 type>,
          更多valueXMap...,
      
          ...
      }
      

      other_component1/Manager

      export let createState = (): state => {
          return {
              maxUID: 0,
              value1Map: Map(),
              ...
          }
      }
      
      export let createComponent = (state: state): [state, component] => {
          let uid = state.maxUID
      
          let newUID = uid + 1
      
          state = {
              ...state,
              maxUID: newUID
          }
      
          return [state, uid]
      }
      
      ...
      
      export let getAllComponents = (state: state): Array<component> => {
          從state中獲得所有的otherComponent1s
      }
      
      export let getValue1 = (state: state, component: component) => {
          return getExnFromStrictUndefined(state.value1Map.get(component))
      }
      
      export let setValue1 = (state: state, component: component, value1) => {
          return {
              ...state,
              value1Map: state.value1Map.set(component, value1)
          }
      }
      
      export let batchOperate = (state: state) => {
          let allComponents = getAllComponents(state)
      
          console.log("批量操作")
      
          return state
      }
      

      有多個(gè)OtherComponentManager,這里只給出一個(gè)OtherComponentManager的抽象代碼

      GameObject的抽象代碼

      GameObjectType

      type id = number
      
      export type gameObject = id
      

      DataOrientedComponent的抽象代碼

      DataOrientedComponent1Type

      type index = number
      
      export type component = index
      

      有多個(gè)DataOrientedComponent,這里只給出一個(gè)DataOrientedComponent的抽象代碼

      OtherComponent的抽象代碼

      OtherComponent1Type

      type id = number
      
      export type component = id
      

      有多個(gè)OtherComponent,這里只給出一個(gè)OtherComponent的抽象代碼

      遵循的設(shè)計(jì)原則在UML中的體現(xiàn)

      ECS模式主要遵循下面的設(shè)計(jì)原則:

      • 單一職責(zé)原則
        每個(gè)System只實(shí)現(xiàn)一個(gè)行為;每個(gè)組件的Manager只管理一種組件
      • 合成復(fù)用原則
        GameObject組合了多個(gè)組件
      • 接口隔離原則
        GameObject和組件經(jīng)過了扁平化處理,移除了數(shù)據(jù)和邏輯,改為只是有一個(gè)number類型數(shù)據(jù)的值對(duì)象
      • 最少知識(shí)原則
        World、System、Manager、Component+GameObject這幾個(gè)層只能上層依賴下層,不能跨層依賴
      • 開閉原則
        要增加一種行為,只需要增加一個(gè)System,不會(huì)影響Manager

      應(yīng)用

      優(yōu)點(diǎn)

      • 組件的數(shù)據(jù)集中連續(xù)地保存在ArrayBuffer中,增加了緩存命中,提高了讀寫的性能

      • 創(chuàng)建和刪除組件的性能也很好,因?yàn)樵谶@個(gè)過程中不會(huì)分配或者銷毀內(nèi)存,所以沒有垃圾回收的開銷
        這是因?yàn)樵趧?chuàng)建ArrayBuffer時(shí)就預(yù)先按照最大組件個(gè)數(shù)分配了一塊連續(xù)的內(nèi)存,所以在創(chuàng)建組件時(shí),只是返回一個(gè)當(dāng)前最大索引(maxIndex)的值而已;在刪除組件時(shí),只是將ArrayBuffer中該組件對(duì)應(yīng)的數(shù)據(jù)還原為默認(rèn)值而已

      • 職責(zé)劃分明確,行為的邏輯應(yīng)該放在哪里很清楚
        對(duì)于只涉及到操作一種組件的行為邏輯,則將其放在該組件對(duì)應(yīng)的Manager(如將batchUpdate position的邏輯放到PositionComponentManager的batchUpdate函數(shù)中);涉及到多種組件的行為邏輯則放在對(duì)應(yīng)的System中(如將飛行行為放到FlySystem中);

      • 增加行為很容易
        因?yàn)橐粋€(gè)行為對(duì)應(yīng)一個(gè)System,所以要增加一個(gè)行為,則只需增加一個(gè)對(duì)應(yīng)的System即可,這不會(huì)影響到Manager。另外,因?yàn)镾ystem只有邏輯沒有數(shù)據(jù),所以增加和維護(hù)System很容易

      缺點(diǎn)

      • 需要轉(zhuǎn)換為函數(shù)式編程的思維
        習(xí)慣面向?qū)ο缶幊痰耐瑢W(xué)傾向于設(shè)計(jì)一個(gè)包括數(shù)據(jù)和邏輯的組件類,而ECS模式則將其扁平化為一個(gè)值對(duì)象,這符合函數(shù)式編程中一切都是數(shù)據(jù)的思維模式。
        另外,ECS中的System其實(shí)就只是一個(gè)函數(shù)而已,本身沒有數(shù)據(jù),這也符合函數(shù)式編程中函數(shù)是第一公民的思維模式。
        終上所述,如果使用函數(shù)式編程范式的同學(xué)能夠更容易地使用ECS模式

      使用場(chǎng)景

      場(chǎng)景描述

      游戲的場(chǎng)景中有很多種類的人物,人物的行為很多或者很復(fù)雜

      具體案例

      • 有很多種類的游戲人物
        通過掛載不同的組件到GameObject,來實(shí)現(xiàn)不同種類的游戲人物,代替繼承

      • 游戲人物有很多的行為,而且還經(jīng)常會(huì)增加新的行為
        因?yàn)槊總€(gè)行為對(duì)應(yīng)一個(gè)System,所以增加一個(gè)新的行為就是增加一個(gè)System。不管行為如何變化,只影響System層,不會(huì)影響作為下層的Manager層和GameObject、Component層

      • 對(duì)于引擎而言,ECS模式主要用在場(chǎng)景管理這塊

      注意事項(xiàng)

      • 因?yàn)榻M件的ArrayBuffer一旦在創(chuàng)建后,它的大小就不會(huì)改動(dòng),所以最好在創(chuàng)建時(shí)指定足夠大的最大組件個(gè)數(shù)

      結(jié)合其它模式

      結(jié)合多線程模式

      如果引擎開了多個(gè)線程,那么可以將組件的ArrayBuffer改為SharedArrayBuffer。這樣的話就可以將其直接共享到各個(gè)線程中而不需要拷貝,從而提高了性能

      結(jié)合管道模式

      如果引擎使用了管道模式,那么可以去掉System,而使用管道的Job來代替。其中一個(gè)Job就是一個(gè)System

      另外,可以去掉WorldState,而使用PipelineManagerState來代替

      最佳實(shí)踐

      哪些場(chǎng)景不需要使用模式

      如果游戲的人物種類很少,行為簡(jiǎn)單,那么就可以使用最開始給出的人物模塊的方案,即使用一個(gè)人物模塊對(duì)應(yīng)一種人物,并通過繼承實(shí)現(xiàn)多種人物,這樣最容易實(shí)現(xiàn)

      更多資料推薦

      ECS的概念最先是由“守望先鋒”游戲的開發(fā)者提出的,詳細(xì)資料可以在網(wǎng)上搜索“《守望先鋒》架構(gòu)設(shè)計(jì)和網(wǎng)絡(luò)同步”

      ECS模式是在“組件化”、“Data Oriented”基礎(chǔ)上發(fā)展而來,可以在網(wǎng)上搜索更多關(guān)于“組件化”、“Data Oriented”、“ECS”的資料

      posted @ 2024-01-19 07:54  楊元超  閱讀(398)  評(píng)論(0)    收藏  舉報(bào)
      主站蜘蛛池模板: 国产美女永久免费无遮挡| 亚洲美免无码中文字幕在线| 国产精品一区在线蜜臀| 久久天天躁狠狠躁夜夜躁2020| 99久久精品国产一区二区蜜芽 | 永和县| 伊人天天久大香线蕉av色| 麻豆国产传媒精品视频| 国产日韩一区二区在线| 国产精品亚洲一区二区三区| 精品国产一区二区三区av性色 | av天堂午夜精品一区| 精品国产午夜福利在线观看| 体态丰腴的微胖熟女的特征| 欧美成人精品一级在线观看| 国产麻豆精品手机在线观看| 视频免费完整版在线播放| 亚欧美闷骚院| 国产欧亚州美日韩综合区| 日韩中文字幕av有码| 成人片黄网站色大片免费| 亚洲国产在一区二区三区| 一本色道久久东京热| 婷婷久久综合九色综合88| 国产精品熟女一区二区三区| 无码成人午夜在线观看| 中文字幕亚洲综合久久青草| 日本熟妇乱一区二区三区| 国产永久免费高清在线观看| 国产不卡免费一区二区| 乱60一70归性欧老妇| 自拍偷拍视频一区二区三区| 国产av成人精品播放| 18av千部影片| 色成年激情久久综合国产| 亚洲 自拍 另类小说综合图区| 日本中文字幕在线播放| 亚洲风情亚aⅴ在线发布| 五月天国产成人av免费观看| 成人国产精品免费网站| 超碰伊人久久大香线蕉综合|