回顧并為今天的內容定下基調
我們正在做一款完整的游戲,今天的重點是“移動模式”的正式化處理。目前雖然移動機制大致能運作,但寫法相對粗糙,不夠嚴謹,我們希望將其清理得更規范,更可靠一點。
目前腦邏輯(AI行為控制)系統已經基本穩定,我們也測試了各種生物的行為,基本可行。接下來的目標,是整理移動模式的系統。現在的移動行為雖然可用,但邏輯分散、不明確,因此今天會花時間來梳理它們。
我們已經進入了行為層代碼的穩定階段,整體架構沒有太多問題,邏輯編寫也較為直接,因此接下來的工作將集中在以下幾個方面:
-
正式引入多種移動模式的定義與處理;
-
精簡和改良世界構建邏輯,目前過于繁瑣,尤其在處理 world chunk(世界區塊)時,參數傳遞較復雜;
-
改善空間查詢系統,讓查詢更加簡潔、高效。
-
一條蛇,會以跳躍方式沿路徑移動;
-
一個“怪物”,會隨機跳躍;
-
一個“familiar”(跟隨者),會靠近玩家位置但行為尚未完善。
這些實體基本都使用了統一的跳躍移動模式(Hopping movement mode),無論是蛇、怪物還是主角都一樣。主角的“頭部”行為稍微特別一些,但也仍在同樣邏輯控制下。唯獨“familiar”例外,它是一個漂浮實體,沒有使用跳躍模式,而是通過大腦邏輯直接移動,類似于直接設置坐標。
然而這個漂浮實體沒有真正地“占據”它所在的格子,仍然保持在初始格子,這種行為顯然不符合預期。我們希望它能具備以下能力:
- 像其它實體一樣,嘗試占據自己移動到的目標格子;
- 如果目標格子已被占據,就不能進入,移動應被阻止;
- 這樣一來,它也能對其他實體產生“阻擋”,而不是隨意穿越。
這種邏輯將適用于所有“漂浮類實體”,例如靈魂、幽靈、魔法球等,它們不應像“投射物”那樣只是穿越,而是要作為完整實體存在,受阻于地形和其他物體。
因此,我們將開始引入一種正式的“漂浮移動模式”(Floating movement mode),并在邏輯上清晰地區分不同的移動類型,而不是當前僅有的“跳躍”和“固定”兩種形式。
這會是我們今天的主要任務。
修改 game_entity.h
,在 entity_movement_mode
中添加 MovementMode_Floating
(漂浮移動模式)
我們查看了實體的定義后,發現每個實體都有一個“移動模式”的枚舉,目前只有兩個值:Planted(固定)和Hopping(跳躍)。大部分實體默認使用Planted模式,而跳躍則是當實體從一個格子跳到另一個格子時才使用的,像主角和部分敵人就是如此。
我們計劃新增一個第三種移動模式,目前暫定名為Sliding(滑行),也可以理解為Floating(漂浮)。我們并不在意實體是漂浮在空中還是平地滑行,關鍵是它不是跳躍,而是一種連續、線性的平滑移動。從功能角度而言,名稱更偏向表達運動形式而不是視覺效果。因此“滑行”更能表達我們實際關心的核心含義。
我們暫定使用三個移動模式:
- Planted(固定):實體固定在某個位置,不主動移動。
- Hopping(跳躍):實體從一個格子“彈跳”到另一個格子,有一個動態躍遷過程。
- Sliding(滑行):實體以連續、線性的方式移動,沒有跳躍軌跡。
雖然我們也考慮過是不是只需要一個移動模式,剩下的只是表現方式不同(比如跳躍其實只是視覺表現),但目前為了代碼清晰、邏輯區分明確,還是保留多種枚舉類型。
此外,我們還考慮了一個更細節的邏輯差異:如果實體是固定在某個格子上(Planted),當格子本身被移動時(例如浮島、可移動平臺等),實體也會跟著移動;但如果是漂浮的(Floating/Sliding),就不應該隨平臺一起移動。這就讓我們有理由保留Floating和Planted作為不同類型。
我們目前位于world mode(世界模式)中處理移動相關代碼的位置,在這里可以看到現有的Planted和Hopping處理邏輯,例如我們使用了一種模擬的彈跳效果來處理跳躍物理。除此之外,當前沒有實現其他物理系統。
接下來,我們還會替換掉現有的MoveEntity
調用,它目前過于復雜,不再適用于我們新的設計需求。當初寫它時主要是為了演示碰撞檢測,所以沒有很嚴謹。現在這個游戲采用“從一個格子跳到另一個格子”的離散移動方式,因此不需要像傳統物理引擎那樣處理“滑動接觸”之類的復雜交互。我們準備重構這一部分,讓它更簡潔、更符合當前設計目標。
總的來說,我們要做的事情包括:
- 引入新的滑行移動模式(Sliding);
- 區分滑行、跳躍和固定三種基本的運動方式;
- 結合實體是否應該隨著其所在格子一起移動的特性,來決定是使用Planted還是Floating;
- 重構
MoveEntity
,避免不必要的復雜邏輯; - 精簡現有的碰撞處理系統,聚焦在格子級別的移動行為上。
這些改動將使我們的實體移動系統更加清晰、有組織,也為后續引入其他移動方式或行為邏輯打下良好基礎。
考慮 MovementMode(移動模式)和 Brain(AI 控制邏輯)之間的職責劃分
我們現在正在處理一個問題:像“familiar”這樣的實體,它在當前的邏輯中是通過設置其 DDP(即目標加速度)來移動的。這種方式的問題在于,當我們直接設定加速度時,它會立即應用于實體本身,但并不會考慮實體當前所處的格子,也不會考慮從哪個格子移動到哪個格子。也就是說,它的移動是完全脫離網格邏輯的,這就導致了一個問題:實體的占用格子(occupy)狀態不會更新。
按照原本的設計,占用格子的更新邏輯通常應該是在 brain(大腦邏輯)層處理的,但現在由于 DDP 是直接生效的,occupy 并沒有得到更新,也沒有網格間移動的概念,這是我們當前需要解決的關鍵問題。
我們面臨兩個設計選擇:
-
在 brain 層處理所有占用和移動邏輯
這種方式類似于當前的跳躍(Hopping)機制,在 brain 中我們決定要跳到哪個格子,并執行跳躍,同時更新占用狀態。 -
將移動細節下沉到實體(entity)層由 movement mode 決定
即在 movement mode 中根據設定的移動類型(如滑行)決定如何從一個格子移動到另一個格子。
目前我們還沒有決定到底采用哪種方式。現在我們有點質疑 movement mode 的定位,因為它既像是控制物理行為的系統,也可能僅僅是視覺表現上的分類。因此我們要重新思考:我們在哪個層次上劃分職責最合理?
另外也考慮過一種可能性,那就是在 brain 層只發出“我要移動”的請求,而具體如何執行交給底層系統。但我們從實現“蛇”這個生物的經驗來看,將 occupy 占用邏輯保留在 brain 層的做法非常高效、簡潔、而且健壯。蛇的運動雖然復雜(長鏈條、多關節運動、避免自撞),但我們只用了很少的代碼就實現了完全正確的行為,而且這個實現不會出錯,不會陷入非法狀態。
這給我們帶來了重要啟發:保持 occupy 操作在 brain 層的好處是簡潔、易維護且邏輯清晰。我們希望保留這種設計優勢,不要因為引入新的 movement mode(如滑行)就打破這種簡潔性。
如果為了引入滑行功能而把職責拆得過于復雜,最終可能導致連簡單的行為都要寫很多重復或冗余的代碼,這不符合我們的設計追求。
所以我們傾向于這樣的設計:
- 腦(brain)層依然負責決策實體想要移動到哪個位置;
- occupy 狀態由腦層根據決策顯式更新;
- movement mode 只作為執行方式的分類,例如跳躍是有垂直位移的,滑行是線性的;
- DDP(加速度)不再直接生效于實體位置,而是用于驅動目標移動意圖,最終轉換為 occupy 的變化。
接下來我們會動手寫一些代碼,幫助我們更清楚地理清這個過程。通過實踐代碼來驗證設計思路,并進一步精煉這套結構,讓系統既靈活又保持我們想要的極簡主義。
修改 game_brain.cpp
,讓 ExecuteBrain
使 Familiar(跟隨者)占據可通行區域并執行事務性移動
我們在 familiar(類似寵物跟隨角色的實體)的移動邏輯中,當前是直接給它設置 DDP(速度意圖),但是我們更想知道的是:我們是否能夠在指定方向上繼續加速移動。換句話說,我們不希望只是盲目地加速,而是要判斷該方向是否可通行、是否有障礙物阻攔。如果前方的格子無法被占用,那么我們希望 familiar 會彈簧回到它當前所處的格子,而不是繼續嘗試進入非法區域。
這個邏輯的實現可以比較簡單:
- familiar 是一個“頭”,我們只在它存在的時候處理邏輯。
- 利用
get_closest_traversable
查找當前 familiar 最近的可通行格子。 - 檢查這個可通行格子是否和當前占用的格子不同。如果不同,說明可以嘗試前往這個新格子。
- 嘗試進行
transactional_occupy
(事務性占用)操作,把 familiar 從當前占用格子移動到目標格子。 - 如果占用成功,說明可以向目標格子移動;否則,我們將 familiar 標記為“阻塞”(blocked)。
我們引入了一個 blocked
變量來處理這個判斷流程:
- 默認假設是被阻塞的。
- 如果當前位置就是最近的可通行格子(說明還在本格子上空),那就不算阻塞。
- 如果
transactional_occupy
成功,也說明不阻塞。 - 其余情況都視為阻塞。
根據是否阻塞,我們采取不同策略:
- 如果阻塞,我們會讓 familiar “彈簧”地回到它占用的格子中心,防止它移動進非法位置。
- 如果不阻塞,并且能找到一個跟隨目標,我們會朝著那個目標移動。
這意味著 familiar 的移動受到了更嚴格的控制,只能進入合法可通行的格子,并且在被阻擋時有回彈機制,從而避免穿越或停留在非法位置。
我們還對部分邏輯做了精簡優化:
- 去掉了原本對“是否需要找目標跟隨”的條件判斷。現在改為:如果被阻塞,就不去找目標,只管回彈;如果不阻塞,再去尋找跟隨對象并調整目標點。
- 對最終目標位置 target 的賦值方式進行統一:先設置為自己所處的格子,然后在非阻塞且有目標的情況下才更新為目標點的位置。
為了讓 familiar 向目標點移動,我們保留了之前的加速邏輯(乘以目標位置距離向量),簡化了處理方式,不再調整太多參數。
最后的效果是:
- familiar 會嘗試移動到最近的可通行格子;
- 如果遇到無法進入的格子,會自動彈回自己當前占用的格子;
- 在可以移動時,會跟隨主角或目標移動;
- 整體保持了一種浮動感,避免非法穿越,同時邏輯簡單清晰。
我們計劃通過進一步調試觀察此邏輯是否如預期運作,并適當調整 spring back 和跟隨機制的細節,使得 familiar 的行為既自然又合規。
調試器:進入 ExecuteBrain
中的 Type_brain_familiar
,調查 Familiar 為何不移動
我們注意到一個奇怪的現象:當前獲取到的最近可通行格(traversable
)與 familiar 頭部所占用的位置(head.occupying
)看起來引用是相同的,但其中一個卻缺少有效的索引。這是個問題,因為我們依賴這個索引來標識該實體在世界中的位置狀態。
我們進一步排查:
- 在默認情況下,我們是從附近的可通行格集合(vicinity)中第一個索引位置(index 0)開始的;
- 通過調用
get_closest_traversable
找到的格子,表面看起來與head.occupying
是同一個實體(引用相等); - 但當我們查看這兩個對象的索引時,發現其中一個沒有有效的 index;
- 這說明雖然它們代表的是同一個實體,但其中一個的索引字段并未被正確設置。
這就引發一個懷疑:是不是我們在某處構造這個對象時,沒有給它設置 index?
于是我們深入查看 get_closest_traversable
的實現細節。我們發現:
- 在該函數中,確實沒有設置返回對象的 index 字段;
- 這就解釋了為什么我們獲取到的 traversable 雖然引用是對的,但索引是空的;
- 這會導致后續判斷(例如是否與當前占用位置相同)失敗,進而影響到阻塞邏輯或位移邏輯。
所以問題的本質是:
我們在
get_closest_traversable
中雖然找到了正確的 traversable 對象,但是返回的 EntityRef 中缺少必要的 index 信息,從而在后續邏輯中與 head 的占用位置產生不一致。
解決思路是:
- 在
get_closest_traversable
中需要明確設置返回的 EntityRef 的 index; - 以確保后續的比較邏輯(包括阻塞判斷、占用切換等)能夠正確執行;
- 否則即便實體引用相等,由于索引為空,整個系統仍然判斷它們不相等,造成錯誤判斷。
我們計劃調整 get_closest_traversable
的實現邏輯,確保其返回的實體引用包含完整的 index 信息,從而保持系統的一致性和穩定性。這樣 familiar 的占用與實際位置匹配,阻塞判斷、回彈和移動邏輯才不會出現誤判。
修改 game_sim_region.cpp
,讓 GetClosestTraversable
設置 Entity.Index
我們認為確實應該這樣做,因為這樣一來,如果我們試圖讓兩個引用(例如 head.occupying
和 get_closest_traversable
返回值)都能正常工作,就可以始終確保它們的字段都被正確設置。具體來說:
- 我們確保
EntityRef
類型在使用時包含了完整的引用信息,包括指向的實體指針和對應的索引; - 如果兩者引用的是同一個實體,但其中之一缺少 index 字段,那么它們在邏輯判斷中會被誤認為是不同的對象;
- 這在像
transactional_occupy
這樣的邏輯中可能導致錯誤的判斷,進而導致 familiar 無法正確地移動或者對阻塞狀態做出反應; - 因此,在我們設置或返回 EntityRef 的地方,應始終明確填充其 index,以避免這種不一致性。
總結就是:
我們決定要修復這一點,確保所有關鍵引用都包含完整的元信息,這樣在判斷實體是否相同時就不會因為缺少 index 而出錯。這是實現邏輯嚴謹性和可靠行為的基本保障。
運行游戲,觀察 Familiar 是否開始移動
目前,我們的熟練體(familiar)只會在確認自己可以進入某個格子時才施加加速度;如果不能進入目標格子,它會施加一個反向加速度回到當前所占據的格子。這種機制基本運作良好,但仍存在一個問題:由于沒有阻尼(damping),熟練體很容易由于慣性而越過目標點,導致振蕩。
我們觀察到的行為是:熟練體會在格子之間不斷擺動,雖然最終會停下來,但在沒有阻尼的情況下會永久震蕩。因此我們需要引入更穩定的控制機制,使其像主角(hero)的 head spring 那樣更有恢復性。
為了解決這個問題,我們考慮引入彈簧阻尼系統,使用一種稱為**比例導數控制器(PD 控制器)**的方式(Proportional-Derivative Controller):
- 比例部分(P):基于當前位置與目標位置的差距,施加一個恢復力,引導對象朝向目標位置;
- 導數部分(D):基于速度或加速度的變化,抑制系統的震蕩,實現更平滑的停止效果。
這種機制就像一個帶阻尼的彈簧,如果我們把當前的位置看作一個質量塊,它在朝目標位置運動過程中不會來回振蕩很久,而是會迅速趨于穩定。
我們在主角的 head spring 上已經實現了這樣的控制方式,因此在熟練體上也采用類似邏輯是合理的。下一步是將這種彈簧控制機制(帶阻尼)整合進熟練體的加速度邏輯中,從而使其行為更穩定、自然,不再震蕩漂移。
總結要點如下:
- 熟練體現在只有在目標格子可達時才施加加速度;
- 若不可達,會嘗試回到當前格子;
- 沒有阻尼會導致持續振蕩;
- 需要引入帶阻尼的彈簧邏輯;
- 采用比例導數控制器(PD 控制)建模運動行為;
- 主角已有類似控制模型,可復用結構與參數。
這樣能確保熟練體在嘗試移動時既具備響應性又保持穩定性,避免過沖與無限震蕩。
黑板內容:比例微分(與積分)控制器
我們目前討論的是比例導數控制器(Proportional-Derivative Controller,簡稱 PD 控制器或 PDC),這是一種常用于模擬彈簧和控制系統中物體運動的控制方法。PD 控制器的作用是根據當前位置與目標位置之間的差距,以及當前速度與期望速度之間的差距,計算出一個加速度,從而控制對象逐步趨近目標狀態。
一、PD 控制器的基本原理
PD 控制器的數學形式如下:
位置的二階導數(加速度):
a ( t ) = K p ? ( x target ? x ) + K d ? ( v target ? v ) a(t) = K_p \cdot (x_{\text{target}} - x) + K_d \cdot (v_{\text{target}} - v) a(t)=Kp??(xtarget??x)+Kd??(vtarget??v)
- a ( t ) a(t) a(t):物體當前的加速度
- x target x_{\text{target}} xtarget?:目標位置
- x x x:當前實際位置
- v target v_{\text{target}} vtarget?:目標速度
- v v v:當前速度
- K p K_p Kp?:比例系數,控制“彈簧”拉力強度
- K d K_d Kd?:導數系數,控制阻尼強度,防止震蕩
這一公式中,加速度的計算源自于兩個差值:一個是位置差,另一個是速度差。每個差值都乘以一個調節系數,分別決定彈簧拉回力和速度校正的大小。
目標是讓這兩個差值都接近零,即:
- 當前的位置靠近目標位置;
- 當前的速度靠近目標速度。
只有當這兩個差值都為零時,系統才達到穩定狀態,加速度為零,物體也不再運動。
二、為什么叫“比例導數”控制器?
命名的由來如下:
- “比例”部分(Proportional):指的是與當前位置誤差成正比的控制項;
- “導數”部分(Derivative):指的是與當前速度誤差成正比的控制項;
但這里提出一個值得思考的觀點:實際上這里的“導數”項看起來更像是“速度差值”而不是嚴格意義上的導數,因此稱為“導數”可能在某些情況下不夠貼切。
我們甚至可以認為這是一個 “比例差值控制器(Proportional Delta Controller)”,因為它控制的是狀態之間的差值(Delta),不管是位置差還是速度差,而非嚴格數學意義上的導數。
三、與積分控制器的比較(PI 控制器)
另一種控制器是 PI 控制器(比例積分控制器):
- 它會累計誤差,即在每一個時間步中都把當前位置與目標位置的誤差累加起來;
- 隨著時間推移,誤差積累得越多,控制器產生的作用力越強;
- 這種方式在偏離目標太久時會產生更強的修正力,適用于消除持續的穩態偏差(steady-state error);
相比之下,PD 控制器更加側重于即時響應和動態調整,而 PI 控制器更擅長處理持續的誤差偏差問題。
四、總結要點
- PD 控制器基于當前誤差進行快速調節,適用于快速趨近目標并抑制振蕩;
- 加速度由位置誤差和速度誤差共同決定,分別由比例項和導數項控制;
- 名稱中的“導數”本質上可以理解為“差值”,因此也可類比為“比例差值控制器”;
- PI 控制器通過累積誤差強化調整力,適合糾正長期誤差,但響應速度較慢;
- 實際應用中常使用 PID 控制器(比例-積分-導數)結合多種優勢。
我們將在熟練體的運動控制中采用 PD 控制機制,使其運動過程既能迅速趨近目標,又能抑制震蕩和來回漂移,提升整體行為的穩定性和自然感。
比例-微分控制器(Proportional-Derivative Controller,簡稱PD控制器)和PID控制器(Proportional-Integral-Derivative Controller)不是同一個東西,但它們有密切的關系。以下是詳細的對比和說明:
1. 定義和組成
-
PD控制器:
- 由**比例(Proportional, P)和微分(Derivative, D)**兩部分組成。
- 控制信號公式為:
u ( t ) = K p e ( t ) + K d d e ( t ) d t u(t) = K_p e(t) + K_d \frac{de(t)}{dt} u(t)=Kp?e(t)+Kd?dtde(t)?- K p K_p Kp?: 比例增益
- K d K_d Kd?: 微分增益
- e ( t ) e(t) e(t): 誤差, e ( t ) = 設定點?(SP) ? 實際輸出?(PV) e(t) = \text{設定點 (SP)} - \text{實際輸出 (PV)} e(t)=設定點?(SP)?實際輸出?(PV)
- 沒有積分(Integral, I)部分。
-
PID控制器:
- 由比例(P)、**積分(I)和微分(D)**三部分組成。
- 控制信號公式為:
u ( t ) = K p e ( t ) + K i ∫ e ( t ) d t + K d d e ( t ) d t u(t) = K_p e(t) + K_i \int e(t) \, dt + K_d \frac{de(t)}{dt} u(t)=Kp?e(t)+Ki?∫e(t)dt+Kd?dtde(t)?- K i K_i Ki?: 積分增益
- 相比PD控制器,PID多了一個積分項。
2. 功能和作用對比
-
PD控制器:
- 比例(P):根據當前誤差 e ( t ) e(t) e(t) 快速調整控制輸出,推動系統向設定點靠近。
- 微分(D):根據誤差變化率 d e ( t ) d t \frac{de(t)}{dt} dtde(t)? 預測系統行為,減少超調和振蕩,提高穩定性。
- 特點:
- 響應速度快,穩定性好。
- 但無法消除穩態誤差(Steady-State Error),即系統可能無法精確達到設定點,存在長期偏差。
-
PID控制器:
- 包含PD控制器的所有功能(比例和微分),并額外增加了積分控制。
- 積分(I):通過累積誤差 ∫ e ( t ) d t \int e(t) \, dt ∫e(t)dt,消除穩態誤差,確保系統最終精確達到設定點。
- 特點:
- 既能快速響應,又能消除穩態誤差,同時保持穩定性。
- 但積分項可能導致超調或振蕩,尤其在調參不當的情況下。
3. 適用場景
-
PD控制器:
- 適用于不需要完全消除穩態誤差的場景,或者系統中已經通過其他方式(如機械設計)消除了穩態誤差。
- 常用于對響應速度和穩定性要求高,但對精度要求不高的系統,例如:
- 某些機械系統的阻尼控制。
- 快速動態響應場景,如無人機姿態控制中的某些部分。
- 優點:結構簡單,響應快,振蕩少。
- 缺點:存在穩態誤差。
-
PID控制器:
- 適用于需要高精度控制的場景,尤其是需要消除穩態誤差的情況。
- 廣泛應用于工業控制、機器人、溫度控制等領域,例如:
- 激光二極管溫度控制(文中例子)。
- 電機速度或位置控制。
- 優點:能實現快速響應、高精度和穩定性。
- 缺點:調參更復雜,積分項可能導致超調或不穩定。
4. PD控制器與PID控制器的關系
-
PD是PID的子集:
- 如果將PID控制器的積分增益 K i K_i Ki? 設置為0,PID控制器就退化為PD控制器:
u ( t ) = K p e ( t ) + 0 ? ∫ e ( t ) d t + K d d e ( t ) d t = K p e ( t ) + K d d e ( t ) d t u(t) = K_p e(t) + 0 \cdot \int e(t) \, dt + K_d \frac{de(t)}{dt} = K_p e(t) + K_d \frac{de(t)}{dt} u(t)=Kp?e(t)+0?∫e(t)dt+Kd?dtde(t)?=Kp?e(t)+Kd?dtde(t)? - 因此,PD控制器可以看作是PID控制器的簡化版本。
- 如果將PID控制器的積分增益 K i K_i Ki? 設置為0,PID控制器就退化為PD控制器:
-
實際應用中的選擇:
- 系統中是否需要積分項取決于具體需求。如果穩態誤差可以接受,或者系統本身有其他機制消除偏差,可以選擇PD控制器,簡化設計和調參。
- 如果需要精確控制(如溫度、位置等必須嚴格達到設定點),則需要使用完整的PID控制器。
5. 文中例子的角度
文中提到,PID控制器可以通過選擇性地使用P、I、D中的一個或多個部分來適應系統需求(例如P、PI、PD或PID)。因此:
- PD控制器是PID控制器的一種特定形式,僅使用比例和微分部分。
- 在激光二極管溫度控制的例子中:
- 如果只使用PD控制器,溫度可能無法精確達到設定點(例如設定25°C,但實際穩定在24.8°C)。
- 使用完整的PID控制器(帶積分項)可以消除這個偏差,確保溫度精確穩定在25°C。
6. 總結
- PD控制器和PID控制器不是同一個東西:
- PD控制器只有比例和微分兩部分,專注于快速響應和穩定性,但無法消除穩態誤差。
- PID控制器多了積分部分,能夠消除穩態誤差,實現更高精度的控制。
- 關系:PD控制器是PID控制器的簡化形式,可以通過將PID的 K i K_i Ki? 設為0來實現。
- 選擇依據:根據系統需求選擇使用PD還是PID。如果需要高精度(無穩態誤差),用PID;如果只追求快速響應和穩定性,且穩態誤差可以接受,用PD。
在 game_brain.cpp
中設置 Familiar 的彈簧效果
我們之前已經詳細討論過相關內容,因此這里不再贅述。我們的重點是:在當前的控制系統中,我們需要引入一些額外的項來提升性能。首先,需要一個足夠“硬”的彈簧(即較大的彈性系數),以確保系統具有足夠的回復力,從而在偏離目標狀態時能夠迅速做出調整。
其次,我們在速度控制方面還需要引入一個負的速度反饋系數。其核心思想是,我們希望系統的目標速度為零,然后減去當前速度,從而得到一個負值作為反饋修正項。為了簡化表達,我們可以直接將負號吸收入系數中,而不必額外計算“目標速度減當前速度”的差值。
因此,我們將速度項的系數設為一個負數,比如暫定為 -4.0,具體數值待后續通過調試(tuning)進一步確定。這樣做的目的是在運動過程中引入阻尼效果,抑制系統的振蕩,增強穩定性。
目前我們已經完成了初步設定,接下來會對這些參數進行調優,找出最合適的取值,使得系統在保持穩定的同時,具備良好的響應速度和誤差修正能力。
運行游戲,查看彈簧效果是否生效
我們注意到當前這個小家伙的移動方式不太令人滿意,雖然總體上它的行為是相對正確的。一個明顯的問題是它的移動顯得有些“粘人”,也就是說它總是緊跟著我們,但最終會在中途停下來。這種停下來的原因是它抵達了某個中間點,而不是始終保持追蹤。
目前我們實現了一個規則,它不會進入我們的方格,我們也不能進入它的方格,這一點是我們希望實現的。但整體表現仍然不盡如人意,這里面可能有幾個原因。
首先,它出現來回振蕩的現象,并不是因為彈簧機制,而是和“阻擋”的概念有關。當它試圖前進時發現目標格子已被占用,于是被迫退回,而一旦退回到前一個格子又檢測到當前格子是可通行的,于是再次嘗試前進,這就導致了往返式的振蕩。這種行為反映出當前系統的一個根本問題——我們沒有機制讓移動中的對象預判前方格子的狀態。它們只有在到達之后,才知道那個位置是否可以占用。
理想情況下,我們希望這個小家伙在即將進入下一個格子前,能夠預測那個格子是否被占用。如果發現被占用,那么它應當直接停在當前格子中,而不是試圖進入目標格子后再退回來。要實現這一點,我們可能需要引入一個新的邏輯,用于提前判斷預期移動方向上的下一個“可通行格”。
這樣一來,當我們知道下一步想移動的位置已經被占據時,我們就不會嘗試去移動過去,從而可以避免那種不必要的前后跳躍和振蕩。同時,我們可以保留當前的“被阻擋邏輯”作為一種安全機制,以防有人在我們決策過程中突然進入了我們原本打算前往的格子。
具體地說,我們需要在做出位置更新決定前,預先判斷這個“目標位置”是否是可通行的。如果不能通行,即使當前并沒有被硬性阻擋,我們也不應該嘗試前往。
接下來,我們準備將這些邏輯進一步整理,封裝成一些輔助函數,以便后續在構建AI控制行為時能夠更方便地復用這些邏輯。我們預期在今后的系統中會大量使用類似的預測與判斷機制,因此將相關代碼結構化處理是非常有必要的。
game_brain.cpp
中引入 TargetTraversable
概念,讓 Familiar 朝目標可通行區域移動
我們想修改角色移動邏輯,使其更具預判性、避免來回振蕩。在現有的邏輯中,我們會在已經嘗試移動之后,才發現目標格子被占用,然后角色再回彈回來。這樣會導致角色在目標與當前位置之間反復震蕩。
我們希望通過以下步驟來優化這個過程:
1. 獲取目標方向
我們已經有了頭部當前位置 Head->P
,還有目標點 ClosestHeroP
。我們先計算出兩者之間的差值 Delta
,也就是移動方向向量。
接著我們對這個向量進行歸一化或零向量處理,用于獲取單位方向。這里可能用到了已有的 NormalizeOrZero
這樣的函數。
2. 預測下一步移動會進入哪個 traversable(可通行格)
我們用歸一化后的方向向量,乘上一個固定的步長(暫定為1單位),相當于“朝目標方向移動一步”。
這樣就能得到預測中的目標 traversable 區塊位置。這個位置不是我們真的去,而是我們“假設”下一步會去那里,用于提前判斷。
3. 判斷目標 traversable 是否被占用
接下來我們檢查該目標 traversable 是否被占用(occupied)。如果沒有被占用,我們再真正地嘗試向這個方向移動。
如果已被占用,我們就不去動它,讓角色當前停留。這種方式就避免了盲目地前進再回彈回來導致的來回震蕩。
4. 當前代碼中缺失的部分
目前我們發現,系統中并沒有一個直接判斷 traversable 是否被占用的函數(比如 IsOccupied()
)。所以我們需要實現這個邏輯。
雖然判斷格子是否被占用是顯而易見的需求,但之前的系統似乎沒有專門抽象出這類函數,這會是后面需要補充的工具方法。
5. 系統中“手動設定”的部分
這里提到了一些邏輯是“寫死”的(baked in),例如:
- 默認前進一步是“1單位”;
- 如何計算 traversable;
- occupied 判斷方式。
這些我們后面都要做成更清晰、通用的接口(systematize),以便更方便地構建智能體行為。
目標效果
最終我們希望實現:
- 移動前預判;
- 只有當下一格未被占用時才移動;
- 避免抖動與振蕩;
- 增強智能體的“感知性”和“預測性”;
- 后續把這一類邏輯抽象成公共的工具函數,用于構建智能行為邏輯(brains)時更容易調用。
這部分改動是系統行為從“反應式”向“預測式”邁出的關鍵一步。
在 game_sim_region.cpp
中引入 IsOccupied
(是否被占用)邏輯
我們希望在判斷某個格子是否被占用時能使用一個更簡單、直觀的方式,而不是手動去訪問 occupier 成員或者其他底層實現細節。雖然判斷某個 traversable 是否被占用是一個非常基礎的操作,但我們仍然希望將它封裝成一個統一接口,比如 IsOccupied()
,這樣后期即使實現邏輯發生變化,我們也無需修改所有使用位置。
1. 簡化判斷方式
我們實現了一個新的封裝函數,比如 IsOccupied(traversable)
,它本質上就是判斷該格子的 occupier
是否為零。如果 occupier == 0
,說明沒有對象占用這個格子,返回 false;否則為 true。
這樣我們在主邏輯中就可以簡單地寫:
if (IsOccupied(targetTraversable)) { ... }
而無需重復寫具體的判斷條件。
2. 函數實現過程中遇到的細節問題
- 初始時因為少了宏定義,比如
32X
,導致了未定義的標識符錯誤(undeclared identifier destrF
)。我們補上了這部分定義。 - 接著程序仍然沒有表現出理想效果,看上去像是角色沒有移動足夠距離。但從視覺表現上看,問題實際上相反——我們可能誤寫了判斷條件邏輯,把“未被占用”誤認為“已被占用”。
3. 邏輯修正后得到正確行為
我們發現之前的判斷中方向反了,把空格當成了有物體占用。所以我們修正了這個判斷的“正負邏輯”。
修正之后,角色開始表現得正常了:只有在目標 traversable 沒有被占用時,才會前進;否則就會停在當前位置,避免了之前的跳動或震蕩問題。
4. 結果驗證
通過視覺調試,我們確認封裝函數 IsOccupied()
生效了。角色的行為變得更加合理、自然,路徑選擇的邏輯更加清晰可靠。
還提到一個“Texture download”的提示,說明程序中其他圖形相關部分仍然正常工作,也可能是在打印調試信息。
總結
我們成功封裝了一個判斷格子是否被占用的統一接口,使邏輯更具可讀性和可維護性。并且通過修正邏輯判斷方向錯誤,解決了實際表現與預期不符的問題。最終使角色移動邏輯更加健壯,為后續擴展打下了基礎。
運行游戲,觀察 Familiar 是否能正確跟隨
我們需要讓判斷目標位置是否被占用的檢測范圍稍微超過一個單位距離。這是之前提到過的一個問題,目前只檢測一個單位的距離是不夠的。
具體來說,如果只判斷向前移動一個單位后的格子是否被占用,可能會導致角色在某些情況下停得過早或者動作不夠流暢。因此,我們希望能檢測一個比單單位距離稍微遠一點的位置,這樣可以更提前地感知障礙物或占用狀態,從而讓角色的移動更加自然和合理。
這部分功能還沒有完全實現,在修正之前,先說明這段思路,以便后續調整代碼時能清楚目標是什么。簡而言之,就是希望能擴展檢測的距離,不僅僅局限于當前位置往前一步,而是能預判更遠一點的目標位置是否可通行。這樣有助于提升路徑規劃和避障的效果。
還是在震蕩
修改 game_world_mode.cpp
,讓 AddStandardRoom
隨機偏移網格點位置
我們當前使用的是規則網格系統,但實際上沒有任何限制必須使用規則網格。我們完全可以使用不規則的位置來放置元素,以此打破對網格的依賴。為了驗證這一點并避免產生誤解,我們進行了一個測試性修改。
在標準房間生成過程中,我們為其中的元素分配了可通行(traversable)屬性,并設置了它們的世界位置(world position)。這些位置本來是固定的,但我們意識到完全可以在這些位置上增加偏移量來打破規則排列。例如,我們可以在X軸和Y軸方向分別添加一個隨機的雙邊偏移(random bilateral offset),使得元素的位置有一定的浮動,從而不再嚴格遵守網格中心的排列。
事實上,我們早就已經在Z軸上實現了類似的偏移,現在只是將這個邏輯擴展到X軸和Y軸。初始嘗試時偏移量設置得過大,導致畫面效果異常,這并不是我們預期的行為。經過檢查,我們發現是誤將原始位置數據完全覆蓋了,忘記加上偏移,而不是在原位置基礎上累加偏移。修正后,我們將偏移改為“加法”,使其疊加在原始坐標上,并將隨機偏移的范圍設定為最大0.25單位,以避免偏移過遠。
這樣做的目的,是為了演示系統并不依賴于固定網格的位置邏輯。我們可以通過靈活地調整實體生成的位置,讓系統在視覺和邏輯上表現出更加自然、不規則的世界布局。這也為后續擴展非網格化的導航和交互方式打下了基礎。
還是不對先看看有什么問題
運行游戲,看到不規則網格上的所有角色都能正常工作
在當前的系統中,我們其實并沒有硬性規定地圖必須是規則網格。我們的代碼從一開始就并沒有強制要求使用網格結構,所有的實體都可以在完全任意的空間位置上運作。唯一真正被嚴格要求的是:在任意時刻,每個可通行的位置只能有一個實體占據。雖然我們目前可能暫時不會利用這個特性,但也不確定未來是否需要用到它,因此我們希望保留這種靈活性,而不是過早放棄。
我們希望將來可以構建一些擁有不同拓撲結構的地圖,所以保留這種“非網格化”能力是很有必要的。這也意味著我們不能依賴某種“邏輯上的鄰近關系”來判斷某個方向上的“下一個格子”在哪。因為在當前系統中,根本不存在所謂“左邊”或“右邊”的格子——沒有嚴格的格子存在。這些點是幾何位置,而不是邏輯順序的網格單元。
因此,我們必須以幾何的方式定義“鄰近點”。比如我們可以說:給我一個方向,在那個方向上搜索,看在多遠的距離內會遇到一個新的可通行位置,這個距離不能太近(避免檢測到自己),也不能太遠(避免跳過目標點)。
我們的思路是通過“方向投射(cast)”來確定要前往哪個下一個點。例如,我們要判斷向左走會走到哪里,就需要從當前位置往左做一個一定距離的投射,找到第一個可以被認為是“下一個”的點。
不過目前階段我們暫時不會專門編寫一個“get_point_in_direction”的查詢函數,因為現在的元素布局還算比較規則,我們可以暫時依賴這種規律來實現所需行為。但在未來做空間查詢時,顯然需要定義一個更通用的“按方向查詢點”的邏輯。
總結來說:
- 我們系統中位置是基于幾何坐標的,而不是基于規則網格;
- 所有實體只能獨占某個位置,不能共用;
- 無法用“左邊/右邊”這種邏輯關系來判斷下一個點,而必須通過方向+距離來幾何投射;
- 當前為了靈活性,我們不打算放棄這種自由布局的特性;
- 未來可能需要更健壯的“方向投射式”空間查詢函數來適配更復雜的地圖結構和導航邏輯。
在 game_sim_region.cpp
中引入 GetClosestTraversableAlongRay
(沿射線尋找最近可通行點)
我們目前正在設想實現一種新的查詢方式,替代現有的“獲取最近可通行點”的方法。現有的方法是基于當前位置直接獲取最近的可通行位置,但我們希望引入一種新的邏輯:從當前位置沿著某個方向發射射線,并獲取射線路徑中遇到的第一個可通行位置。雖然現在我們還不會正式實現它,但我們想先設計出這個接口,這樣在未來真正需要實現時,我們可以知道哪些地方已經在使用它,便于后續替換和優化。
因此,我們提出了一個假設函數叫 GetClosestTraversableAlongRay
。這個函數的目標是:
- 在一個模擬區域中,從一個起始點出發,沿著某個指定方向發出一條“射線”;
- 沿著這條射線采樣多個點;
- 找到沿這條射線第一個遇到的可通行點;
- 跳過起始點自身,以免誤判當前點為“下一個”點;
- 雖然目前我們還未進行優化,只是用一種相對低效、粗略的方式實現采樣邏輯,但這些后續都可以進行加速處理。
我們目前打算復用已有的、較慢的點查詢邏輯做一個簡單的占位實現,只要可以實現基本功能即可,性能暫不考慮,后續會重構并優化。
此外,由于我們要沿著一條射線去查找目標,而不是簡單最近點查詢,我們還得特別處理一個問題:如何正確“跳過”當前位置,避免返回自身作為目標。這是因為在連續采樣過程中,第一個返回的點很可能是當前所在位置,因此我們需要加入一些邏輯來跳過當前格子或者做一些排除判斷。
接下來我們計劃在實際代碼中調用這個新函數名,雖然暫時還沒有真正實現它,這樣我們可以清晰地知道它的用途,并在后續需要真正實現時迅速定位調用處和邏輯影響。
總結如下:
- 我們希望支持一種“朝某方向查詢第一個可通行點”的新方式;
- 這比“找最近點”更加符合智能導航的思路;
- 盡管現在我們不會立即優化它,但會先設計接口、寫個基礎實現;
- 必須跳過當前所在點,避免錯誤判斷;
- 未來這些射線查詢邏輯將成為空間導航功能的重要組成部分;
- 所有這些操作都是為了支持靈活地圖和更智能行為決策。
在 game_brain.cpp
中讓 ExecuteBrain
使用 GetClosestTraversableAlongRay
控制 Familiar
我們正在實現一個新的函數,用于沿著射線方向尋找可通行點。這個函數比原來直接獲取“最近可通行點”的方法更加智能和靈活,適用于當前不處于規則網格的系統結構。
我們設想該函數 GetClosestTraversableAlongRay
接收以下參數:
- 當前模擬區域(sim region)
- 起始位置(head P)
- 方向向量(Delta)
- 一個用于跳過的目標,即不希望返回的可通行點
我們希望它能做以下幾件事:
- 不要返回當前位置所在的可通行點,這是為了防止因為自身剛好就是一個可通行點而被錯誤返回。
- 不需要過濾被占用的可通行點,因為我們正是要檢測目標是否被占用,所以要保留這些信息。
為實現這個目標,我們使用如下策略:
- 設置一個循環,用于在射線上探測多個采樣點(例如最多采樣5次,探測深度遞進);
- 每一次探測都通過起始點加上方向向量乘以步長來獲得一個新采樣點;
- 步長暫定為0.5單位,這樣采樣更密集;
- 對每一個采樣點,調用現有的
GetClosestTraversable
方法; - 如果返回了一個可通行點,我們還需要判斷這個點是不是我們打算跳過的那個(也就是當前位置所對應的那個);
- 如果返回的結果不是跳過的點,則說明找到目標,標記為已找到并退出循環;
- 如果沒找到,則繼續采樣直到探測完畢。
整個過程總結如下:
- 利用方向向量,從當前點出發向外逐步采樣;
- 每個采樣點調用可通行點查詢邏輯;
- 跳過起始點代表的通行點,避免誤判;
- 一旦找到合適結果就立即停止;
- 此方法目前為簡化實現,后續可根據需求優化采樣策略與性能。
這個新方法將作為臨時版本使用,為未來引入更高效、更結構化的空間查詢邏輯(如射線投射、空間索引等)打下基礎。我們當前的目標是確保在非規則地圖結構下,依然可以靈活地向任意方向查找下一個可通行單元,不依賴嚴格的網格約束。
運行游戲,Familiar 表現更穩定
現在角色的運動方式更加穩定了,表現也更加自然,不再出現之前那種奇怪的停頓行為,除非靠近我們需要停下來時才會出現短暫停止。
接下來我們繼續測試,把更多的角色放入場景中,以觀察整體行為和穩定性。同時我們也意識到一個改進點:希望能觀察到這些角色在什么時候認為自己被阻擋了,以及他們的阻塞狀態,這對于調試行為邏輯非常重要。
目前這些角色分散地留在地圖上,也帶來一種有趣的視覺效果。不過時間所剩不多,因此我們準備進行一些收尾工作,優先處理當前能完成的部分。
一個接下來的小目標是增強可視化調試功能,例如:
- 顯示某個角色是否處于被阻擋狀態;
- 在屏幕上標注他們的阻塞原因;
- 更方便地跟蹤他們的路徑決策和狀態變化。
雖然這些功能暫時還沒有完全實現,但已確定其必要性,并計劃在后續進一步加入。我們希望通過這些增強手段,更清楚地掌握角色的移動狀態與行為邏輯,提升整體系統的可控性和可維護性。
在 game_sim_region.cpp
中為 GetClosestTraversable
和 GetClosestTraversableAlongRay
添加 TIMED_FUNCTION
性能標記
我們現在關注的重點是想了解某些函數實際運行時消耗了多少時間。之前已經為此寫了一些工具,不過具體的函數名記不太清了,可能是 TimeFunction
之類的。我們的目標是給某些函數加上時間統計,查看它們在運行時的性能開銷。
我們決定前往 sim_region.cpp
文件中查看和設置這些時間統計的調用,因為我們懷疑程序中大量的性能損耗其實是來自這些空間查詢(spatial queries)。這些查詢往往會遍歷大量數據,因此速度很慢。
雖然目前還不能確定,但我們的直覺是,正是這些空間查詢函數消耗了程序的大部分時間,因此希望通過加上時間分析來驗證這一點。一旦加上 TimeFunction
,我們就能從性能分析報告中確切地看到這些函數所占用的時間比例,從而指導我們接下來的優化方向。
運行游戲并查看性能分析器
我們在性能分析中觀察到,大約 15% 的時間都消耗在 GetClosestTraversable
函數中。這非常符合預期,因為該函數執行了大量的空間查詢操作,而這類操作本身效率低下、計算密集。雖然目前函數邏輯較為簡單直接,但這種結構很適合進行高效優化,提升潛力非常大。
從另一個角度看,這是件好事:因為這意味著瓶頸集中在一個易于識別和隔離的地方。針對該函數,我們可以引入更高效的空間數據結構(例如空間哈希、網格劃分、四叉樹等)來加速鄰近對象的查找,理論上可以將函數執行時間降低到幾乎可以忽略不計的水平,從而顯著提升整體模擬效率。
與此同時,我們注意到 BeginSim
函數也消耗了大量的處理時間。目前尚不完全清楚其具體的性能瓶頸位置,不過初步判斷是在執行數據解包的過程中,因為其中存在三重循環和對大量數據塊(blocks)的逐一處理。在這些處理邏輯中,大量結構體被復制、移動,很可能是性能開銷的主要來源。
為驗證這一點,計劃進一步對 BeginSim
的內部執行流程進行精細的時間標記,將整個函數過程拆分為更小的片段進行單獨計時。重點關注:
- 三重循環中的迭代次數和內存訪問模式
- 數據塊的解包過程是否涉及大量結構體復制或內存分配
- 是否有重復無效的操作或可以重構的流程
通過這些更細致的觀察,可以找出具體耗時點,并進一步評估是否可以通過數據結構優化、使用引用代替值傳遞、減少緩存未命中等方式進行性能優化。
總的來說,目前定位到的兩個主要性能瓶頸都比較明確,其中 GetClosestTraversable
的優化空間更大,且改動相對獨立易控,是非常理想的優化目標。BeginSim
雖然復雜一些,但其內部結構也比較清晰,后續分析后同樣有望進行結構化優化,從整體上提高系統的模擬效率。
在 game_sim_region.cpp
中引入 GetClosestEntityWithBrain
和 closest_entity
(最近帶 AI 的實體)
我們實現了一個新的功能:在模擬區域中查找最近的、具有特定腦類型(brain type)的實體。這個查詢功能通過一個叫 GetClosestEntityWithBrain
的方法來實現。我們提供一個起始點(位置)、一個模擬區域、以及目標腦類型作為輸入,然后返回一個結構體,包含查詢結果的各項信息。
為了提高函數的實用性,我們還加入了默認參數。默認情況下,如果調用者不指定搜索半徑,我們使用一個默認值(例如20米),這樣調用方在不關心搜索范圍時也可以直接使用函數,保持接口簡潔。
在結果結構中,我們不僅返回是否找到目標實體(found),還包含了:
- 找到的實體引用(entity)
- 距離平方(distance squared)——這個在很多邏輯中非常有用,可以避免額外的平方根運算
- 位移向量 Delta —— 從當前點到目標實體的向量差,這樣使用者就無需再做重復的向量計算
在查詢過程中,我們遍歷模擬區域中的所有實體,對每個實體計算其與起點的 Delta 向量和距離平方,并根據這些值判斷是否更新當前最優(最近)結果。所有計算都被明確地保存下來,以便復用,避免重復計算浪費性能。
此外,為了代碼清晰和避免表達式嵌套引起重復計算,我們顯式地計算了 Delta 向量,并將其單獨保存,而不是嵌入在某個 if 判斷或函數調用中。
當前版本還比較基礎,未來可能需要擴展查詢方式的靈活性,例如允許使用更復雜的匹配規則或過濾條件。但由于 C++ 的閉包傳參機制并不簡便(尤其在沒有使用現代 lambda 的情況下),我們暫時沒有引入通用的謂詞接口,以避免引入不必要的封裝開銷和語法復雜性。
總體而言,這個功能實現了靈活且高效的空間實體查找機制,具備良好的擴展性和性能優化空間,同時提供了簡潔的接口供上層邏輯調用,后續可根據需要進一步擴展查詢粒度和篩選方式。
在 game_brain.cpp
中讓 Familiar 使用 GetClosestEntityWithBrain
實現更智能的追蹤
我們將空間查詢的調用方式做了優化,原先在執行一大段邏輯來查找具有特定“腦類型”的實體,現在將這部分邏輯提取出來,通過調用一個通用函數 GetClosestEntityWithBrain
來完成。
我們傳入模擬區域(sim region)、當前位置(head P)和想要尋找的腦類型(brain type),然后函數就會返回最近符合條件的實體及相關數據。使用這個函數之后,原來的代碼就能大幅簡化,不需要再重復寫一堆循環和判斷邏輯。
我們進一步清理了代碼結構:
- 不再需要自己計算目標實體的位置偏移(delta),因為
GetClosestEntityWithBrain
已經把這個值作為返回結果的一部分。 - 如果函數返回一個實體,我們就直接使用它,不需要檢查額外的“found”標志位,因為只要返回的實體非空,就說明找到了。
- 原來為了排除重復或處理狀態而引入的一些判斷邏輯,也可以取消,比如 ray stamping 相關的標記,因為新的查詢方式已經簡化了流程,不再依賴那些中間狀態。
因此,最終我們只需根據返回結果進行方向歸一化(normalize),然后使用這個方向進行后續的導航或決策。通過這種方式,我們將空間感知與實體決策代碼解耦,大大提高了可讀性和重用性,同時也為未來的維護和優化打下了基礎。
運行游戲,Familiar 行為表現良好
我們對代碼進行了整理和簡化,現在通過封裝好的工具函數來處理查找特定“腦類型”實體的邏輯。這個新的通用查詢工具極大地提升了代碼的清晰度和可維護性:
- 提供了一個統一接口,可以用于查找任意類型的目標實體,不再需要手動重復編寫循環查找邏輯。
- 封裝的結果結構中包含了是否找到目標、目標實體本身、與當前位置的偏移量(Delta)以及平方距離等信息,這樣其他模塊在使用這些數據時可以直接復用,無需重新計算。
- 通過設置默認的搜索半徑參數,也方便了不關心具體范圍的調用者。
- 所有原先零散實現的功能,例如計算偏移、判斷最近距離等,現都整合到一個地方,便于集中優化和未來維護。
- 此外,之前多余的變量、狀態判斷邏輯也被清除,使主邏輯更簡潔、更聚焦于行為本身。
借助這個查詢系統,我們可以很方便地在模擬區域中定位具有指定智能行為的實體,并對其作出決策或交互反應。這種結構化的改進,不僅提高了整體代碼質量,也為后續擴展新功能或提升性能提供了清晰的切入點。至此,這部分的重構已圓滿完成。
進入問答環節
對于實體系統的多線程處理有什么想法?
我們在討論將能量系統進行多線程化的可能性時,首先明確了一點:是否能多線程以及該如何多線程,取決于我們具體想并行化的部分。
我們識別出兩個方向:
-
明確并行的目標任務
不是所有能量系統的邏輯都適合并行。例如,如果某些能量計算之間存在高度依賴性(比如狀態更新彼此依賴),那么強行并行可能引發競態條件或同步復雜度過高。因此,需要優先分析哪些部分是數據獨立、可并發執行的。典型的例子可能包括對多個實體獨立能量狀態的評估更新。 -
并行粒度與同步策略
如果我們打算并行更新多個實體的能量狀態,我們可能需要引入某種任務分發機制,將實體劃分為批次分配給不同線程進行處理。這要求我們在設計時控制好內存訪問,避免多個線程同時訪問或修改同一內存區域。也就是說,在這些更新中需要做好同步,可能需要使用互斥鎖、原子操作或線程局部存儲等手段,或者通過任務分區避免交叉訪問。
此外,我們還注意到性能的提升不一定線性,要根據系統瓶頸來決定是否值得并行化。如果能量系統本身耗時較少,多線程反而會引入不必要的管理成本。
總結來說,我們準備進行如下步驟:
- 識別系統中耗時較多、數據互不依賴的處理邏輯。
- 將這類任務劃分為線程安全的批處理單元。
- 使用任務系統或線程池結構來分發計算任務。
- 控制數據共享和同步開銷,防止并發沖突。
- 在具體實現之前,做好性能分析,以判斷并行化是否帶來實質性收益。
整體上,這種多線程策略的核心是“目標明確,數據獨立,同步可控”。只要把握住這幾點,能量系統就有可能通過并行處理獲得可觀的性能提升。
使用 ++ 和 += 減少內存使用……
我們目前在思考如何對系統進行多線程優化,歸納起來主要考慮兩個方面:
一、多線程優化當前實體模擬(entity sim)
我們認為暫時沒有必要對當前的實體模擬進行多線程加速,原因如下:
- 當前性能尚未成為瓶頸:我們目前在一個模擬區域內運行了數百個實體,但在Release模式下,它幾乎沒有在性能分析中留下痕跡,說明它運行非常快。
- 尚未進行任何優化:實體模擬系統還沒有進行過系統性的優化,現階段對其進行并行處理屬于“過早優化”,可能效果有限且得不償失。
- 當前硬件性能較強:我們運行的硬件性能并不差,實體模擬的負載目前完全可以承受。
因此,我們認為現在將實體模擬進行多線程化并不是一個明智的投入方向,至少在系統中其他更耗時部分優化之前不值得優先考慮。
二、在世界范圍內對遠離玩家的區域進行并行模擬
這是我們更感興趣的并行方向,原因如下:
-
SimRegion是并行隔離的:每個SimRegion的更新完全在其自身作用域內進行,不會影響其他區域。這意味著可以很容易地將不同的SimRegion交由不同線程處理而無需擔心數據競爭問題。
-
線程安全設計已初步具備:當前系統的設計本身就考慮到了可擴展性,尤其是多線程方向。只需要對以下少數部分做線程安全處理:
- ID分配(如獲取新實體ID等)需確保使用原子操作。
- 某些可能使用普通加法的邏輯需要替換為原子加法(例如對全局統計變量的修改等)。
-
有助于世界擴展性:當世界中存在多個遠離玩家的位置同時需要進行模擬更新時,可以將它們分配給多個工作線程,實現“真正的并行模擬”,這對性能提升非常有幫助。
三、未來可能需要并行的部分(例如AI)
目前我們還未遇到這類情況,但預判到將來可能會存在某些非常重型的AI計算或路徑規劃(如復雜導航網格查詢等),這類運算可能不能簡單地放進主線程,屆時可能會考慮將這些AI行為拆分到獨立線程進行處理。
總結
我們當前的多線程策略可分為幾個階段:
- 短期內不對當前實體模擬并行處理,因為它尚未成為瓶頸。
- 中期將系統結構設計成多SimRegion并行更新模型,這是可行的,也是當前架構支持的。
- 長期考慮將高耗時AI邏輯分離為異步處理,根據具體需求評估是否需要。
總的來說,我們已經具備了向多線程擴展的基本條件,并在架構上有意識地避免了線程干擾,為未來的高并發處理打下了良好基礎。
你還要被紋理上傳問題坑幾次才會修它?
我們遇到了一些問題,具體表現為在修復之前會先被問題影響,比如出現渲染紋理上的問題,但現在并不適合立刻去修復這些問題。原因在于當前處于開發過程中的關鍵階段,沒有時間去處理這些細節故障,因為修復這些問題可能會打斷正在進行的工作流程。盡管如此,這些問題并不會導致系統崩潰或影響整體運行,只要關閉程序然后重新啟動就可以暫時規避。預計這些問題會等到以后重新回到渲染相關工作時才會被正式解決。目前的策略是保持整體工作的連貫性和穩定性,避免在關鍵時刻去修補非致命的細節問題。
你在調試顯示中是否有模擬區域內最大實體數?
。在模擬區域開始時(begin sim),理論上在整個流程結束時應該能夠調用一個編輯賬戶的操作,但目前還不確定這個值具體會是什么。通過實際測試,當前數值大約是740,當走到有更多對象的地方時,數值會漲到1121。令人驚訝的是,現在的機器性能非常強大,即使在發布版本中,對1100多個實體每秒運行16幀的處理,幾乎沒有在性能分析中顯現出明顯的負擔。這主要是因為我們做的其實并不是傳統意義上的數據打包解包,而是塊復制操作,這種操作效率極高。現代計算機的速度遠超我們的預期,這也是為什么網絡速度常常顯得很慢而令人不滿的原因。整體來看,計算機處理速度非常快,盡管用戶體驗中有時候感覺不到。最后,目前沒有更多關于編碼的疑問。