在這次開發的遊戲中,角色有不同的移動方式,像是玩家角色會以方向做為移動輸入,而敵人角色則是以路徑做為移動目標,而這兩者又有以距離為主的移動方式,如:瞬步、攻擊墊步。所以一開始做了通用的移動模組來整合這些方式,讓所有角色使用。但隨著敵人角色的移動方式增加,為了讓模組能保持「通用」,部份功能只能做在模組之外,造成移動方式無法集中管理。最後受不了重新設計移動模組一番,讓模組能夠為不同的角色提供各自需要的功能,又可以保持增加移動方式的彈性。

通用模組的問題

一開始通用模組設計如下圖:

original-design-uml

  • 通用移動模組 CharacterMovementModule 中提供不同的移動方式的子模組:
    • 靜止的 IdleMovement
    • 以速度向量為主的 VelocityMovement
    • 以距離為主的 DashMovement
    • 以路徑為主的 PathMovement
  • 移動模組同時只會有一個子模組在執行,當子模組執行結束後(如:移動路徑走完了),會自動轉到 IdleMovement
  • 尋路用的模組 PathFindingModule

提供用不到的功能

這是遊戲中各類角色需要的移動功能:

移動方式 玩家角色 敵人角色 NPC
靜止 :heavy_check_mark: :heavy_check_mark: :heavy_check_mark:
速度向量 :heavy_check_mark: :x: :x:
距離 :heavy_check_mark: :heavy_check_mark: :x:
路徑 :heavy_check_mark:(路徑更新一次) :heavy_check_mark:(路徑定期更新) :heavy_check_mark:(一次跟定期更新都要)

設計成通用模組的問題是,使用的角色會有用不到的功能。敵人角色用不到速度向量的移動功能,而 NPC 一樣用不到速度向量的移動功能之外,也用不到距離為主的移動功能。

不易擴充

另外從上面的表可以看到,以路徑移動的形式有兩種:只要更新一次路徑(移動到指定位置)跟定期更新路徑(持續追隨)。為了讓移動模組能「通用」,所以只提供傳入路徑再移動的功能。這時玩家角色需要實作讓尋路模組來找路徑後傳入移動模組的功能,而敵人角色則需要實作定期更新路徑的功能,然而 NPC 卻兩種形式都要,所以就得要再各實作一次一樣的功能。雖然可以把這兩種路徑移動形式連同尋路模組一起整合到移動模組內,但這樣移動模組就會相依於尋路模組,而且爾後有需要其它模組的移動方式的話,都會讓移動模組相依更多模組。如果有新的角色是不需要路徑移動方式的話,為了移動模組還要再生成尋路模組就顯得多餘了。

original-design-coupling 讓模組相依於其它模組的問題是無論用不用的到對應的模組,都得要生成依賴的模組

重構版本

這是重構後的版本:

refactor-design-player-uml

  • 移動方式各自獨立成外部子模組,繼承自 IMovement,並自行提供所需要的建構子與初始化函式
  • CharacterMovementModule 只保留移動方式切換的功能,透過 IMovment 介面存取移動方式子模組
  • 角色需要實作自己所需的移動模組,像玩家要實作 PlayerMovementModule,在裡面建立與初始化所需的移動方式子模組,並註冊給 CharacterMovementModule 管理
  • 角色自己的移動模組也會存建立的移動方式子模組,在需要切換移動方式時,先設置好參數,再切換移動方式

如此一來,通用移動模組就不用依賴其它模組了,而是由角色自己的移動模組傳入需要的模組。另外移動方式獨立成子模組後,就可以像積木一樣,由角色自己的移動模組拼出所需要的移動功能。如敵人與 NPC 的移動模組會像這樣:

refactor-design-enemy-uml refactor-design-npc-uml

另外兩種不同的路徑移動方式也分成路徑走一次的 PathMovement 與路徑定期更新的 FollowMovement,而這兩個移動方式的共同邏輯則是取出做成一個 PathHelper 來幫助執行路徑移動。

總結

使用情境差不多的話,做成通用模組是合適的方法,而且一開始規格還不明確的時候,太早設計成這樣也可能殺雞用牛刀。但當出現一些清況,就得要開始思考這樣做適不適合了,像是:

  • 有部份功能只是為了某些使用者存在
  • 模組是不是開始相依其它模組的功能
  • 每次加入新功能時,模組邏輯是否得要大幅修改
  • 為了配合新功能,其它使用者是否也要改變使用方式

如果有這樣的情況就得要考慮重構了。