回顧并為當天的工作做準備
我們有一個關于玩家移動的概念,玩家可以在點之間移動,而且當這些點移動時,玩家會隨之移動。現在這個部分基本上已經在工作了。我們本來想實現的一個功能是:當玩家移動到某個點時,這個點能“知道”玩家是占據它的人,如果有其他人想移動到同一個點,則不允許這樣做。為此,我們希望建立一個簡單的事務系統,來管理點的占用情況,方便測試和控制。除此之外,暫時不處理房間中帶有斜角或坡度的復雜情況,所以打算關閉房間斜坡這一特性,先專注于點的事務性占用管理。
game_world_mode.cpp:禁用坡度功能
把那條線去掉后,構建物體的Z軸偏移就變成了零,這正是我們想要的效果。這樣房間就是一個平坦的空間,可以正常跳躍。場景里還有一個小角色,如果時間點合適,可以跳到這個移動的角色身上,測試時保留這個功能沒有壞處。
接下來,我們要重新整理“站在某物上”的概念。為了實現這個功能,我們需要一個登記機制,也就是說,要有一個系統能夠記錄誰站在哪個點上。這樣模擬區域(sim region)才能知道某個點已經被占據,不允許其他人去那里。如果那個點是空的,系統就可以告訴玩家那里是可去的。
為此,我們需要寫一些代碼來跟蹤這個狀態。實現方式有幾種,比較合理的是直接在實體的解包(unpack)過程中處理,因為解包時必須知道這個實體對應的是哪個可通行點(traversable)。在這里直接存儲哪個實體占據了該點,是最簡單直接的做法。于是接下來就要看看如何具體實現這個方案。
game_entity.h:在entity_traversable_point中添加了一個指向實體的指針 *StandingOn
在實體結構中已經有了可通行點(traversable point)的概念,它存在于碰撞體積(collision volume)內。如果想的話,可以把某個實體和這個可通行點關聯起來,比如說給這個點一個實體引用,表示某個實體正站在這里。
其實不需要把這個信息打包保存下來,因為實體本身在運行時就知道自己站在哪個點上,所以只需存儲實體指針即可。這個指針只需要在模擬區域(sim region)內有效,保存和加載數據時可以忽略它。打包(pack)時不需要特殊處理,指針可以不寫入保存的數據;解包(unpack)時再更新這個指針。
具體來說,在世界打包操作時,打包實體到數據塊(chunk)、打包可通行引用、打包實體引用時,都不需要額外操作,因為這個指針最終會被丟棄。解包時,在加載可通行點數據的第二遍處理中(connect entity pointers 階段),會進行指針的更新和關聯工作。
總體思路是在創建和加載實體時,合理處理這些指針的賦值和更新,使得實體和它所站的可通行點能夠正確對應。
game_sim_region.cpp:讓LoadTraversableReference在實體有指針時寫入實體的碰撞體積
在加載實體引用時,不僅僅是簡單地加載實體引用本身,而是要在加載完之后,檢查加載到的實體是否有效。如果有效,就需要更新該實體的碰撞體積中的可通行點信息,明確告訴它“這個可通行點現在被誰占據了”,也就是說,讓這個點知道當前站在上面的是哪個實體。
為了做到這一點,在加載可通行引用時,需要知道這個引用的來源實體是誰,把這個信息在解包過程中保存下來。然后在解包時,將這個信息寫入對應的實體,使得實體的碰撞體積中的可通行點能夠正確記錄當前站立者。
具體實現時,加載時知道當前處理的實體,因此可以正確更新指針和狀態。當加載完成后,相關指針就會被正確更新,實體和它所站的可通行點之間的關系得以正確維護。這樣才能保證模擬區域內部關于“誰站在哪個點上”的信息是準確的。
game_world_mode.cpp:如果traversable點被占用,則為其著色
為了觀察角色在不同可通行點上跳躍時的情況,我們打算根據這些點是否有實體指針關聯來給它們著色,從而直觀地看到每個點的狀態。剛開始顏色顯示可能會不準確,因為我們還沒有清除之前的狀態,但至少可以通過顏色的變化來追蹤代碼的運行效果。
具體做法是在繪制可通行點時,使用繪制矩形輪廓的方法,但注意到這些點本身沒有碰撞器,所以繪制時要特別處理。在繪制過程中,會檢查該可通行點的顏色狀態,進而反映是否有實體“占據”它。這樣,通過顏色就可以清楚地看到哪些點被占用,哪些點是空閑的,有助于調試和后續開發。
先把邏輯和繪圖分開一下之前沒做這個
運行游戲,發現traversables已經帶有新的顏色
我們原本預期這些可通行點的狀態在運行時會立即發生變化,但實際觀察后發現它們并沒有如預期那樣立即更新,原因在于這些碰撞點結構是共享的。這意味著多個實體實際上引用的是同一組數據,因此在一個地方修改并不會體現為獨立的、個體化的狀態變化。這顯然不是我們想要的行為,因為我們期望每個實體能獨立維護自己的狀態。
這個問題引發了一個關鍵的設計思考:是否還要繼續讓這些結構在多個實體之間共享,還是應當將它們“展開”成為實體自身的一部分,也就是說,把數據寫入每個實體自身中,而不再是共用一份。這是一個不太容易決定的問題。
從資源使用角度來說,如果這些數據非常龐大,比如每個實體要占用 64KB 的碰撞數據,那么共享是顯然更合理的。但當前的情況是這些數據體積其實不大,因此是否共享并不是明顯的優劣之分。
我們進一步分析當前的結構發現,這些碰撞體其實就是某種“碰撞組”,大多數情況下它們都是類似的,例如很多地面磚塊的可通行點都設在同一個位置(如 0,0,0),所以共享是合理的。但考慮到我們設計的是一個表達能力極強、可配置性很高、偏向“有機生長”的實體系統,最終的傾向是——每個實體應該有自己獨立的 traversable(可通行)點,而不是共享的。
這樣做的好處是:每個實體的可通行點可以隨意變動、被修改,并且不會影響其他實體的行為。這種靈活性更適合我們當前正在構建的系統風格。至于存儲開銷問題,盡管每個點可能需要 8 或 12 個字節的空間,也許我們可以通過建立一個索引表的方式,將它們壓縮為一個字節來引用,從而節省內存。
總之,在共享結構和獨立結構之間,我們更傾向于選擇讓 traversable points 成為每個實體自身的組成部分,以支持更強的靈活性和表達力,盡管這會增加一定的內存使用和管理復雜性。
game_entity.h:將Traversable設為實體的一個屬性
我們決定不再將可通行點(traversable points)作為碰撞組(collision group)的一部分。相反,我們將其作為實體(entity)本身的屬性進行管理。這么做的原因是我們希望每個實體都可以擁有自己的可通行點,并直接附帶占用者(occupier)信息,這樣能避免共享結構帶來的混亂,并使系統的表達能力更強。
為了快速實現這個系統,我們初步采用了硬編碼的方式,為每個實體設定一個固定數量的可通行點,使現有代碼能繼續運行。不過,我們意識到,這也許是一個合適的時機,順勢將可通行點設計為一個靈活長度的數組。這樣,不同實體就可以根據需要擁有不同數量的可通行點,從而實現更復雜的交互邏輯與空間結構。
具體實現上,我們不再通過碰撞組來訪問可通行點數據,而是直接在實體結構中維護這些數據,包括可通行點的位置和對應的占用者指針。這也簡化了很多代碼邏輯,因為不再需要從碰撞體組中間接地查找數據,改為直接從實體結構中讀取和寫入。
在處理過程中,我們還需要確保所有創建實體的代碼路徑都被更新,以正確初始化其可通行點。例如,在構造地面區域時,我們將可通行點的位置設定為目標位置,同時將其占用者設置為空。這樣,一旦實體被放置在該點上,占用者就可以被正確設置和跟蹤。
測試運行時我們發現了一個有趣的現象:由于初始只在一個特殊的“上下移動平臺”上添加了可通行點數據,導致玩家立即跳到了那個唯一擁有可通行點的實體上。盡管這不是預期效果,但也印證了我們的邏輯修改生效了。為了解決這個問題,我們把相同的初始化邏輯也添加到其他普通地面實體中,使整個系統恢復到與之前一致的運行狀態。
總的來說,我們已經完成了將可通行點從共享結構中獨立出來,并與實體自身綁定的改造。這為后續實現更復雜的占用控制邏輯和實體交互打下了堅實基礎,也提升了整個實體系統的靈活性和可配置性。
怎么占用的沒變顏色呢
EntityType_Floor 沒設置顏色的原因
讓共用一個case
case EntityType_Floor:
case EntityType_FloatyThingForNow:
運行游戲,控制Pacman移動經過traversable點
我們在移動過程中可以觀察到,每當實體跳到一個新的可通行點上時,該點的占用者指針就會被設置,表明它被占用了。這部分機制已經運行正常,但目前仍存在幾個關鍵問題需要解決。
首先,占用者指針一旦設置,就永遠不會被清除。當前的實現邏輯僅在解包(unpack)階段設置占用者指針,也就是說,只在幀與幀之間的狀態重建時,根據上一幀的位置將當前實體標記為占用了某個可通行點。這顯然是不夠的,因為我們并未在實際移動實體的邏輯中更新或清除這些占用狀態。這意味著,一旦某個可通行點被標記為已占用,就不會因為實體移動離開而恢復為空,這會導致錯誤的占用狀態殘留。
其次,目前系統還無法阻止多個實體同時占用同一個可通行點。這是我們接下來需要重點解決的問題。為此,我們需要引入一個新的邏輯概念:在嘗試占用某個可通行點之前,必須檢查它是否已經被其他實體占用了。只有當目標點為空時,才能完成占用;否則,應該拒絕此次移動。
接下來的工作將圍繞這一目標展開。我們將深入檢查處理實體移動的代碼路徑,增加以下幾個關鍵步驟:
- 在每次嘗試移動之前,查詢目標可通行點是否已經有占用者;
- 如果沒有占用者,允許移動,并更新占用者指針為當前實體;
- 如果已經有其他實體占用,則拒絕移動請求;
- 每次實體從原位置離開后,要清除原可通行點上的占用者指針。
實現這些步驟之后,我們才能真正建立起一個有效的**“事務性占用”系統**:每個可通行點最多只能被一個實體占用,系統會自動管理占用與釋放,避免位置沖突,也為后續的角色交互、碰撞管理、AI移動控制等提供了必要的基礎邏輯支持。
game_world_mode.cpp:在跳轉到traversable點之前檢查該點是否可用
當前的代碼體系中,還沒有建立一個良好的權限判斷機制。現在的做法是在移動實體時,直接設置目標位置的占用者,而沒有經過任何檢查與驗證。也就是說,我們默認目標位置是空的,這在多實體交互或者競爭式空間占用場景中是明顯不合適的。
具體來看,在嘗試讓某個實體移動到某個可通行點時,代碼中直接將目標可通行點標記為該實體正在占用,這種“盲寫式”的處理方式正是當前系統的核心問題所在。為了解決這個問題,我們計劃引入一個事務式占用系統(transactional occupy),它可以安全地決定是否允許某個實體占據指定的可通行點。
我們要做的是構建一個新的函數接口,例如叫作 TransactionalOccupy
,用來表達這樣一個意圖:“我(某個實體)希望將自己某個槽位中的位置設置為此目標可通行點,請問可以嗎?” 這個函數會返回一個結果,說明此次請求是否被允許。
如果請求被批準(目標位置未被占用),我們就可以開始跳躍動作;如果請求失敗(目標已被占用),我們就不執行移動。此外,為了保持邏輯清晰,我們打算使用兩種不同的失敗路徑來區分:
- 一種情況是“我們并沒有嘗試移動”;
- 另一種情況是“我們試圖移動,但失敗了”。
這個區分很重要,因為控制邏輯(如AI或玩家輸入處理)可能需要根據失敗類型做出不同反應。舉例來說:
- 如果我們從未嘗試移動(因為玩家沒有輸入方向),那控制器不需要做任何事;
- 如果我們嘗試了但目標點被占用,那么控制器可能會選擇等待、重試、改變策略或尋找替代路徑。
為此,我們打算在初步實現中將這兩種情況明確分開處理,以便后續擴展控制邏輯。
綜上,事務式占用機制的核心作用有三點:
- 確保空間的互斥使用(一個可通行點只能被一個實體占據);
- 為控制器提供明確的反饋路徑(嘗試失敗 vs 未嘗試);
- 為未來行為系統和AI設計打好基礎(如擁擠檢測、阻塞狀態響應等)。
這將是整個移動系統從被動單向“寫入”,轉向主動、安全、多實體并發協同的關鍵一步。
game_sim_region.cpp:引入TransactionalOccupy和GetTraversable函數
目前正在實現一個“事務式占用”機制,用于判斷實體是否可以占據某個目標可通行點,并安全地進行占用與釋放操作。
首先,我們在模擬區域(SimRegion)中創建了一個新的操作接口 TransactionalOccupy
,其目的是在實體嘗試移動到目標位置之前進行判斷和處理。如果成功,則更新占用信息;如果失敗,則不允許移動。
步驟與實現細節:
-
準備訪問接口
為了方便訪問實體的可通行點數據,我們實現了一個GetTraversable
函數,允許通過實體指針和下標來獲取對應的可通行點。這包括處理邊界檢查(如斷言)以避免越界訪問,確保數據安全。 -
事務式占用邏輯
在TransactionalOccupy
函數中,傳入兩個參數:destRef
:當前實體原本所在的位置引用(destination)desiredRef
:當前實體想要移動到的位置引用(desired)
接著,我們根據
desiredRef
獲取目標的 traversable 點desired
,檢查其是否已被占用。- 如果目標未被占用:允許占用,設置其占用者為當前實體。
- 如果目標已被占用:不允許占用,操作失敗。
-
原位置釋放占用
如果允許占用新位置,還要同步地釋放舊位置的占用信息:- 通過
destRef
獲取當前占用點dest
; - 將其占用者設置為無(null),表示當前位置被釋放。
然后將
destRef
更新為desiredRef
,表示實體現在正式處于新位置。 - 通過
-
默認返回值處理
初始實現中忘記返回成功標志,因此即便操作成功,也被認為失敗,導致移動邏輯無法執行。已修正為在成功設置占用時返回true
。 -
非初始化引用處理
特別注意,destRef
可能是無效或未初始化的。在這種情況下,GetTraversable
應該返回一個空值或零結構,防止非法操作。實現中已添加條件分支判斷是否為有效實體指針,僅在有效時才執行訪問。 -
位置計算時的安全性考慮
對于獲取可通行點后的位置偏移計算也添加了保護機制:如果GetTraversable
失敗,就保留當前位置;否則才會基于獲取到的偏移進行實際位置更新,避免因無效引用導致坐標錯誤。 -
擴展與復用
同樣對其他涉及位置偏移的邏輯(如空間計算)也做了類似修改,統一使用GetTraversable
接口,確保邏輯一致性與安全性。
總結:
這一階段完成了從“直接寫入占用信息”到“事務式、安全驗證”的過渡,實現了實體之間空間互斥的基礎設施。事務式占用機制:
- 避免多個實體占據同一位置;
- 清晰管理占用和釋放邏輯;
- 為未來AI路徑判斷、阻擋檢測提供了清晰入口;
- 提高系統健壯性,避免非法訪問和狀態錯誤。
接下來可進一步考慮將此機制融入移動控制器、導航系統中,實現完整的路徑判斷和動態調度能力。
跳過程有問題
運行游戲,發現幾個問題
我們現在遇到了一些問題,尤其是關于“占用”的定義和行為上的混淆。
第一個問題:頭部和身體的占用狀態不明確
我們目前尚未清楚區分實體“頭部”和“身體”在占用格子上的行為。從觀察結果來看,好像兩者都在嘗試占用格子,這導致了邏輯上的混亂。
具體來說:
- 英雄角色的“頭部”似乎沒有設置任何占用信息,但又出現在占用邏輯中。
- 這說明頭部可能在邏輯上是“經過”某格子,而不是“站在”其上。
- 我們現在的系統沒有明確地區分“正在移動中(Moving To)”和“實際站立中(Standing On)”的狀態,這就使得“占用”邏輯很模糊。
第二個問題:事務式占用沒有區分移動路徑與最終落點
目前的 TransactionalOccupy
并沒有處理“移動中間狀態”的特殊性。在角色“跳躍”或者從一個格子移動到另一個格子的過程中,我們處于兩個格子之間,這種狀態下是否應該算作“占用”某個格子是不明確的。
問題在于:
- 當前只要進行移動就會立即設定目標格子的占用者。
- 然而,在實際表現中,實體還處于兩個格子之間。
- 原來的格子是否應該立刻釋放占用?新的格子是否應立刻占用?這些邏輯尚未理清。
- 沒有精細區分“開始移動”、“占據中”、“完成移動”三種階段。
第三個問題:解包操作(Unpack)導致了錯誤的占用狀態
目前在 LoadEntityReference
和 LoadTraversableReference
等解包邏輯中,也對占用字段進行了操作,但這實際上是不合理的。
我們認為:
- 占用邏輯應只在真正“站立”時觸發,而不是在數據被讀取或解包時。
- 解包的目的是還原狀態,而不是建立新的占用關系。
- 如果在每一幀解包中都更新占用者,就會導致原本已移動走的實體仍然被視為占用目標格子,造成邏輯沖突。
下一步計劃
我們必須重新審視整個“占用”系統的模型,并劃清如下幾個關鍵概念:
- 正在移動中(Moving):實體處于動畫過程中的過渡狀態,不應修改占用狀態。
- 實際站立中(Standing):實體處于穩定位置,應該唯一地占據一個可通行點。
- 解包狀態恢復(Unpack):用于還原數據,不應引發副作用,如設定占用信息。
為了解決這些問題,我們需要:
- 為“占用”狀態設置明確的觸發時機,只在完成移動并落定的時刻才設置。
- 抽象出“當前站立位置”這一角色的狀態字段,僅該位置用于處理占用邏輯。
- 修改
TransactionalOccupy
,增加狀態判斷,確保邏輯只對實際占據格子進行處理。 - 重構解包邏輯,避免在數據加載階段意外更改占用狀態。
總結
目前的占用機制尚不完善,核心問題在于:
- 沒有區分“移動中”與“占用中”;
- “頭部”是否占用與邏輯模型不一致;
- 解包操作引發副作用;
- “占用”機制需要被更精確地建模為一個狀態機。
后續我們需要圖示與狀態劃分輔助理清這些邏輯,并對移動系統與控制器進行聯動更新,以保證狀態的正確傳播和處理。
Blackboard:從Standing On狀態切換到Moving To狀態
我們當前面臨的核心問題是關于“跳躍(hop)”過程中格子占用的時機控制。
問題描述
假設角色從格子 A 跳躍到格子 B,這個過程中涉及兩個關鍵狀態:
- Standing On(站在):表示當前實際站立并占用的格子;
- Moving To(移動至):表示移動目標格子,即接下來要跳到的地方。
目前系統存在的問題在于,我們可能同時占用了兩個格子(A 和 B),但沒有在合適的時間釋放舊格子(A),也沒有很好地管理何時接管新格子(B)。
占用時序圖分析
如果用時間軸來表示占用關系:
- 初始狀態:我們占用格子 A;
- 跳躍過程中:要么同時占用 A 和 B,要么必須在合適的時間釋放 A 并接管 B;
- 最終狀態:只占用格子 B。
正確情況:
-
A 和 B 占用期間無空檔,可以是:
- 重疊:在占用 A 的同時已經占用了 B;
- 無縫切換:恰好在釋放 A 的同時接管 B。
錯誤情況:
-
A 和 B 之間存在空窗期,即兩個格子在某個時間都未被占用:
- 會出現其他實體可能在這段時間內“插隊”占用 A 或 B;
- 造成跳躍失敗或沖突,邏輯上不可接受。
當前邏輯的問題
目前我們在跳躍開始時直接占用了兩個格子(A 和 B),而沒有釋放 A,也沒有延遲接管 B 的機制:
- 沒有一個明確的機制在跳躍落地時釋放 A;
- 在跳躍過程中,系統無法區分“即將離開”與“尚未占用”的中間態;
- 實際造成了“冗余占用”和“占用狀態滯后”。
可行方案探討
我們必須重新設計跳躍過程中的占用邏輯。這里有幾種思路:
方案一:只占用“當前站立”格子(A),不占用“目標格子”(B)
- 在跳躍過程中,我們只保持對 A 的占用;
- 等到跳躍真正完成(落地幀),再嘗試事務式占用 B;
- 如果占用失敗(例如 B 被別人先占了),跳躍失敗或中斷。
優點:邏輯清晰,始終只占一個格子
缺點:有可能在空中時 B 被別人搶先占用,導致落地失敗,影響體驗
方案二:在跳躍開始時占用 B,同時延遲釋放 A(即暫時占用兩個格子)
- 在跳躍開始幀,同時占用 A 和 B;
- 跳躍結束幀,釋放 A,僅保留 B 的占用;
- 保證整個跳躍過程中不會有人“插入”到 A 或 B 中間。
優點:跳躍過程更安全無沖突
缺點:短時間內兩個格子被占用,可能導致空間利用率下降
關鍵原則:
- 不能出現“中間無人占用”的狀態,即 A 釋放后 B 未占用;
- 至少占用一個格子,最多同時占用兩個(過渡態);
- 釋放和接管必須是事務式:要么全部成功,要么都不動。
下一步實施方向
- 明確跳躍起始與落地的幀;
- 將
TransactionalOccupy
改為能夠表達“延遲釋放”和“預占用”的模式; - 在跳躍起始時占用 A + 事務嘗試占用 B;
- 如果成功,則標記為“雙占用中”,跳躍結束幀釋放 A;
- 如果事務占用 B 失敗,則取消跳躍或重新規劃。
總結
我們的問題本質上是“如何保證跳躍過程中占用狀態的連貫性”。當前的方式占用了兩個格子但未及時釋放,導致狀態殘留。而真正合理的處理是以時間為核心控制點,事務化操作跳躍起點與終點的占用權轉移,避免空窗、沖突與資源競爭。接下來需要在 entity
邏輯中重構跳躍過程的狀態切換點。
game_entity.h和game_world_mode.cpp:假設占用1個格子的實體一次只占用一個格子
我們決定調整實體在跳躍過程中對格子的占用邏輯,核心目標是整個跳躍過程中任意時刻只占用一個格子,從而避免復雜的雙格占用邏輯和可能引發的資源沖突問題。
核心假設
我們現在設定:
實體在任何時刻只占用一個格子(tile),也就是說:
- 如果是站立狀態,那就只占用“站在”的格子;
- 如果是移動過程中,那就只占用“目標”格子;
- 或者某些特殊實體(如 Boss)天然擁有多個格子的占用面積,這種情況就視為實體的“體積”更大,這些情況獨立處理。
我們不打算讓普通實體在跳躍期間同時占用“來源”和“目標”格子。
數據結構調整
我們引入一個字段:occupying
,表示當前實體真正占據的那個格子。同時添加一個 came_from
字段,用于記錄上一個來源格子。
這兩個字段將配合使用:
occupying
:當前正在占據的格子;came_from
:跳躍中記錄的來源位置,但不占用。
Pack 階段處理邏輯
在數據打包階段,我們有兩個相關字段:
standing_on
(站立的格子);moving_to
(即將移動的目標格子);
我們要明確只有 occupying
字段真正改變格子的“被占用”狀態,而 came_from
只是一個輔助記錄,不參與實際的占用邏輯。
行為邏輯改變
以前的邏輯中,在跳躍過程里實體可能會:
- 同時“站在”A;
- 同時“移動到”B;
- 并且 A 和 B 都會被標記為“被占用”。
現在的邏輯調整為:
-
當跳躍開始時:
- 僅將目標格子設為
occupying
; - 來源格子記錄為
came_from
,但不再保留“占用”狀態;
- 僅將目標格子設為
-
跳躍結束后:
- 實體的位置更新完成;
- 只保留對目標格子的占用;
came_from
無需再使用,可清空或重置。
適應場景擴展
這個方式的一個好處是,它能夠支持更通用的情況,例如:
- 支持大體積實體只在每一幀更新中明確自己占用的多個格子;
- 支持不同實體根據狀態決定當前是否需要釋放/接管格子(例如漂浮單位可以“占用零個”格子);
- 簡化事務化占用邏輯的判斷條件(只判斷目標格子是否空即可,不必處理舊格子釋放沖突)。
總結
我們現在的策略是:
- 每個普通實體始終只占一個格子;
- 用
occupying
明確記錄當前占用位置; - 跳躍中不再占用“來源”格子;
- 用
came_from
輔助記錄來源,供跳躍動畫等使用,不影響占用狀態; - 特殊情況(如大體積單位)可通過不同機制處理。
這樣,整個系統更加清晰、高效,并避免了跳躍過程中的競爭條件和占用沖突。
運行游戲,發現traversables的著色按預期工作
現在我們可以看到,藍色的占用效果已經如預期般工作了。當角色移動穿越時,一旦角色的頭部進入目標格子,系統立刻將該格子標記為被占用,然后跳躍完成。這表示實體在跳躍過程中對目標格子的接管是即時且準確的。
當前跳躍機制表現回顧:
- 在角色“頭部”進入目標格子的一刻,目標格子就會被設為
occupying
; - 原來格子的占用狀態會被釋放;
- 整個過程保持了實體在任意時刻只占一個格子;
- 視覺表現也能反映出這一機制,邏輯清晰可靠。
接下來打算實現的功能:
我們希望能在運行時動態添加多個英雄角色,便于測試和驗證多人情況下的占用機制是否正確運行。
因此,我們計劃引入一個快捷鍵,按下后可以立刻在場景中添加一個新英雄。
實現該功能的意圖:
-
驗證并發占用邏輯:
檢查當多個角色在相近區域跳躍、移動時,是否能夠正確處理格子占用沖突,避免重疊。 -
測試事務占用系統:
新增角色時,必須確保其初始位置不會占用已被他人占用的格子。可以觀察事務性占用邏輯是否阻止了非法占用。 -
提升開發效率:
多角色切換與插入更利于調試跳躍過程、占用狀態同步、視覺表現等復雜交互。
后續預期效果:
引入該功能后,我們將能夠在多個實體之間進行跳躍測試,模擬真實的游戲場景,確保:
- 不同角色之間不會“踩到”彼此的格子;
- 跳躍過程中如有沖突,事務性占用邏輯能及時阻止跳躍;
- 占用狀態始終保持干凈清晰,無殘留占用狀態。
這個功能將成為我們調試并驗證整個跳躍系統穩定性的重要工具。
game_world_mode.cpp:重新實現添加新英雄的功能
當前目標是重新實現“添加玩家”的功能,使其在游戲運行過程中能夠通過按鍵動態地添加一個新的玩家實體。
當前已有的機制分析:
目前已有一段添加玩家的邏輯,是通過 controller_start_add_player
實現的,作用是在游戲啟動或初始化階段添加一個玩家。但我們現在的需求,是在游戲運行中可以按鍵動態添加玩家,用于調試和驗證系統功能。
新的實現思路:
我們打算將這段“添加玩家”的邏輯,移動到游戲主邏輯中,并由某個按鍵觸發,這樣我們可以隨時添加新的玩家而不是只能在初始化時添加。
具體做法:
-
確定觸發按鍵:
- 我們目前還沒有決定使用哪個按鍵;
- 想先臨時用一個調試用的按鍵來實現,比如
Alt
鍵,稍后可以更換; - 需要檢查該按鍵是否會和現有邏輯沖突。
-
修改按鍵輸入邏輯:
- 在輸入處理代碼中,添加對所選按鍵的檢測;
- 當檢測到按鍵按下,調用
add_player
邏輯; - 添加之前先檢查是否已經添加過相同的玩家,防止重復添加。
-
防止重復添加:
- 如果
entity index
已經存在于玩家控制列表中,則不再添加; - 這一點需要在輸入邏輯或添加邏輯中加以判斷。
- 如果
實施效果預期:
- 游戲運行中按下某鍵,會即時創建并添加一個新玩家;
- 多個玩家可以同時存在,用于測試碰撞、跳躍、占用等機制;
- 添加行為受控,不會造成重復添加或狀態混亂。
后續考慮:
- 根據開發需要,將調試用按鍵替換為更合適的組合鍵或 UI 按鈕;
- 每個新玩家可自動分配不同的控制方式或顏色以區分;
- 可以進一步擴展成一個“玩家管理面板”用于動態添加/移除/控制多個實體。
這個改動是整個調試過程中非常有幫助的一步,它會顯著提升我們在多人交互、路徑沖突等復雜場景下的測試效率。
運行游戲,嘗試添加新英雄,享受怪異的效果
目前我們實現了目標功能:可以在游戲運行時通過按鍵動態地添加玩家角色,同時角色間還具有簡單的物理交互,例如碰撞和彈跳。雖然彈跳現象有些奇怪,但它確實是因為之前引入的物理系統所產生的結果。
現狀總結:
- 新增玩家角色功能已完成;
- 玩家角色被添加后可以與現有角色發生碰撞;
- 碰撞后會出現彈跳效果,這是因為物理系統自動處理了接觸響應;
- 控制器仍需完善,尤其是在處理新玩家加入后的控制綁定問題。
存在問題與細節優化:
-
速度未歸零
新角色在添加時如果不重置速度,可能會出現意外移動或殘留運動狀態。
需要在角色初始化或添加時將其速度向量設為零。 -
頭部尋路邏輯過于自由
當前系統允許角色的“頭部”可以在未受限制的情況下移動至任意可達點,甚至跳過被占用的位置。
需要改進跳躍/移動邏輯,優先判斷目標點是否可占用,否則跳過或嘗試其他位置;
增加跳躍動作對占用信息的依賴,從而限制不合理的遠距離移動行為。 -
啟動流程被破壞
新的玩家加入邏輯改變了系統對“已有玩家”的判斷方式,影響了啟動流程中模擬按鍵行為的部分邏輯。
系統原本通過檢測是否存在英雄角色來決定是否觸發“開始游戲”模擬操作;
修改后在游戲開始時沒有檢測到英雄角色,從而導致行為失效。
解決方向:
- 在游戲初始化階段,確保角色添加邏輯在“開始游戲”模擬操作之前完成;
- 優化“是否已有玩家”判斷機制,使其兼容動態添加方式;
- 增強輸入系統支持,比如針對每個玩家分配唯一控制器,防止控制沖突;
- 針對頭部與身體的移動分離邏輯,引入更精確的狀態判斷,比如“準備跳躍”“正在跳躍”“著陸中”等狀態。
當前效果總結:
- 功能上達成了預期目標;
- 存在一些細節問題需要進一步梳理和優化;
- 整體邏輯仍處在搭建階段,需要對控制器管理、物理交互、占用狀態管理等模塊繼續完善。
后續的工作應集中在提升系統的穩定性與可控性,特別是在處理多個實體并存時的行為一致性和輸入管理層面。整體方向正確,已經邁出了關鍵一步。
game_world_mode.cpp:考慮讓AddPlayer函數更健壯
當前我們正在處理游戲角色動態添加過程中出現的一些結構性問題,主要集中在模擬區域(Sim Region)與實體(Entity)系統之間的數據同步與更新機制。以下是對當前邏輯與存在問題的詳細總結:
現有邏輯分析:
-
添加角色(Add Player)流程問題
- 當前
add_player
函數在角色添加時,并未直接將實體加入到模擬區域中,而是只將其添加到了全局存儲(World Storage)中; - 因為模擬已開始運行,模擬區域不會自動感知到全局存儲中的新實體;
- 導致某些邏輯(例如判斷是否有角色存在)失敗,因為這些邏輯依賴的是模擬區域中的數據而非全局存儲。
- 當前
-
英雄存在判斷失敗
- 啟動邏輯中通過檢查模擬區域中的英雄實體來判斷是否已有人物加入;
- 新增角色未注冊到模擬區域中,判斷邏輯自然失敗;
- 雖然角色實體在系統中存在,但在模擬視角下“并不存在”。
問題的根本原因:
- 實體被添加進了“錯誤的”地方——它進入了“世界存儲”,卻未被推入當前活躍的“模擬區域”;
- 當前
add_player
邏輯繞過了模擬區域的標準流程,導致“添加”在邏輯上不完整; - 缺乏一個統一的、上下文感知的實體添加流程。
優化建議與設計方案:
-
建立統一的實體添加路徑
-
創建一個通用的
add_entity
接口,該接口根據當前是否處于模擬模式決定將實體:- 直接加入到模擬區域(Sim Region);
- 或者推入世界存儲區(World Storage);
-
保證代碼邏輯一致,避免手動分支導致的同步問題。
-
-
模擬區域即時注冊機制
- 當處于模擬狀態時,應立即將新實體加入模擬內存塊;
- 可通過封裝一個“即時解包并寫入 SimSlot”的函數來實現。
-
考慮結構效率與一致性平衡
- 雖然直接寫入模擬區域實體槽效率更高,但需要手動維護結構完整性;
- 而統一走 Pack → Unpack 流程可以保持所有代碼路徑一致、易維護,雖然性能略低;
- 在當前階段,優先推薦保持結構一致性,后續可根據性能需求優化為直接寫入模式。
-
明確實體生命周期管理
- 無論實體在哪添加,都應保證其“在模擬幀結束時被視為正式注冊”;
- 避免中間狀態導致更新邏輯混亂。
總結:
- 當前角色添加流程存在結構漏洞,需引入統一的實體注冊機制;
- 模擬區域與世界存儲的數據需通過接口協調統一,不能分別處理;
- 優化建議為:封裝帶上下文判斷的
add_entity
接口,根據是否處于模擬狀態決定添加路徑; - 可以繼續采用 Pack → Unpack 模式保證邏輯一致性,后續再做效率優化;
- 修復此問題能大幅提升代碼穩定性與行為一致性,是值得優先解決的基礎問題。
該問題雖小,但若任其發展,將對后續多人交互與動態行為系統造成結構性隱患,因此應及時整理修正。
game_world_mode.cpp:在兩種情況下都設置HeroesExist,運行游戲后決定保持AddPlayer原樣
目前我們決定暫時采用現有的解決方式,其修復邏輯非常簡單明確。我們判斷是否已創建英雄,只需通過檢查是否存在即可。這樣實現起來直觀清晰。
進一步思考后發現,目前采用的“打包再解包”(Pack → Unpack)流程其實也許正是最合理的方式。這種方式結構非常對稱、統一,不需要為特殊情況單獨編寫路徑邏輯,系統在結構上更加簡潔穩定。
尤其是這個流程具備一些額外的優勢:
保持結構統一性:
- 無論實體是何時創建的,它們都走相同的流程:打包 → 寫入世界 → 在下一幀開始時解包進入模擬;
- 所有對象都在幀開始統一加載,沒有中途插入的行為,減少狀態混亂風險。
確保時間一致性:
- 所有實體都會在下一幀開始時加入模擬區域;
- 不會出現某些實體在一幀中途加入而其他實體已完成更新的“半狀態”問題;
- 這一時序上的純粹性可以為未來的行為同步、回放重構等高級機制打下良好基礎。
代碼簡潔且易維護:
- 不需要增加對“當前是否處于模擬模式”的判斷;
- 添加邏輯保持通用和中立,降低了邏輯分歧和維護復雜度。
當前階段決策:
- 決定繼續沿用現有的打包解包添加流程;
- 不再嘗試中途將實體直接加入模擬區域;
- 采用“下一幀生效”策略,實現簡單、邏輯清晰、結構統一、狀態純粹。
后續優化方向(可選):
- 若將來對效率有更高要求,仍可考慮為少數高頻添加場景優化直接插入流程;
- 但需要確保不會破壞當前時間同步和結構對稱性的優點。
綜上,當前策略已滿足正確性與可維護性的平衡點,因此決定保持現狀,不再修改,暫時告一段落。
問答環節
這個調試世界每天都變得越來越怪異
確實,調試階段中的世界狀態常常變得非常混亂,這種混亂幾乎可以說是調試代碼的常態。在這個過程中,時常會產生一些“調試怪物”,各種看上去不正常甚至“恐怖”的狀態和現象層出不窮。
調試過程中常見的問題:
- 對象狀態異常:某些數據未初始化或狀態不一致,會導致角色位置、動作等出現異常;
- 視覺畸變:尤其是在圖形編程中,比如搞錯了矩陣變換或順序,哪怕只是個微小的變換 bug,結果就可能是角色被極度拉伸、壓扁甚至完全“解構”;
- 資源錯位或重疊:在物理系統或碰撞系統中,如坐標計算錯誤、同步失敗等,常會導致角色彼此重疊、互相彈飛、卡死等問題;
- 邏輯錯位:比如我們剛才討論的實體是否正確進入模擬區域、何時被添加等問題,在調試狀態下往往更容易暴露不一致性。
2D 程序相對溫和:
相比 3D 來說,2D 調試的“畫面災難”要輕一些。因為我們只處理平面信息,就算出現錯誤,也只是表現為對象錯位、壓縮、拉伸等,在視覺上不至于那么“驚悚”。不像 3D 中,一個錯誤的骨骼變換或不對稱矩陣可能直接導致角色身體被反折、肢體亂飛等。
當前場景下的問題:
我們在調試過程中發現:
- 新增的克隆角色有些行為不符合預期;
- 出現的某些視覺與行為異常也許只是由于調試狀態下數據未初始化或狀態殘留;
- 雖然能運行,但邏輯上不夠健壯、容易出現邊緣行為;
- 這種混亂在實際開發中難以完全避免,需要通過明確劃分“調試模式”和“正式模式”來應對。
結論與建議:
- 調試代碼常常會制造臨時性的混亂結構,接受它是過程的一部分;
- 2D 視覺上的容錯性比 3D 高一些,適合初期原型與調試驗證;
- 應盡早為調試代碼構建清晰的邊界和隔離機制,避免其影響正式流程;
- 克隆角色的狀態要嚴格初始化,避免未定義行為蔓延至主邏輯。
調試世界的混沌是構建復雜系統的“副產物”,我們需要的不是避免混亂,而是有能力從混亂中快速提煉出清晰問題并修復它。
為什么克隆的玩家頭部移動到點上時不跳過去?
關于角色頭部移動到某個點時是否觸發跳躍的問題,這是一個值得深入考慮的問題。當前代碼是在角色頭部的控制器綁定中處理這件事的,但其實這并不是必須的。完全可以選擇在角色頭部控制器綁定之外的地方處理這部分邏輯。
通過觀察代碼,可以看到角色頭部的控制器綁定中確實用到了相關邏輯,并且其作用范圍在一定時間和空間上是有限的,最終會停止執行。舉例來說,可以把跳躍觸發的邏輯從頭部控制器中抽離出來,放到更上層或者獨立的系統中處理,這樣可能會使整體設計更加靈活和清晰。
總結來說:
- 目前頭部移動觸發跳躍的代碼綁定在頭部控制器中;
- 這種設計可以被替換或優化,比如將觸發邏輯放到控制器綁定之外;
- 這樣做可以使代碼結構更合理,也方便以后對跳躍機制進行擴展或調整;
- 頭部控制器對跳躍邏輯的作用是有限的,有時可能不需要完全依賴它來決定跳躍行為。
game_world_mode.cpp:使跳躍動作無論是否由控制器控制英雄都發生
代碼里提到一個計時器“timer recenter”的原因可能是因為代碼需要知道它的狀態,不過可以調整設計,使得跳躍功能不依賴于“connected hero”(連接的角色)這一概念,這樣會更簡單。
具體思路是,給被控制的主角設定一個“dummy”(虛擬)控制器,比如叫 con_hero_
,這個控制器其實什么都不做,就是一個空殼。然后,在代碼中初始化時,將“connected hero”設為這個空的、非活動狀態的控制器。
之后,程序會檢查所有的控制器(比如四個控制器),看看有沒有哪個是和當前主角連接的。如果找到了,就用那個控制器;如果沒有找到,就用之前的那個“dummy”控制器。這樣就保證了無論是否真的有連接的控制器,后續的代碼都能正常運行,不會因為找不到“connected hero”而出錯。
總結如下:
- 目前跳躍功能依賴“connected hero”,增加了耦合和復雜度;
- 通過創建一個空的、無效的虛擬控制器來替代“connected hero”的缺失,保證程序的健壯性;
- 檢查所有控制器,找到真實連接的那個,如果找不到,就用虛擬控制器代替;
- 這樣設計后,跳躍等功能就不必強依賴是否有連接的控制器,提升代碼靈活性和容錯能力。
運行游戲,生成大量英雄
代碼設計調整后,無論是否有控制器連接到英雄頭部,相關邏輯都會運行。這樣,正常情況下角色跳躍和移動功能不會受影響,即使沒有人為控制,代碼依然會執行。
當切換英雄時,即使控制器暫時沒有連接,控制英雄頭部的代碼也會繼續運行,保證邏輯的連續性和穩定性,不會因為沒有連接的控制器而中斷或出錯。這種設計增強了系統的魯棒性,避免了因為控制器連接狀態變化導致的邏輯異常。
game_world_mode.cpp:保持這些英雄不斷跳躍
英雄頭部有一個彈簧機制,始終將頭部拉近身體,因此英雄會一直停下來。如果想讓英雄朝某個方向移動并跳躍,就需要在沒有連接控制器的時候,為英雄施加一個推動力。
具體來說,當英雄沒有連接控制器時,需要給英雄的加速度(比如ddP.X)賦一個非零值,這樣英雄才會有方向上的運動,才能看到跳躍的動作。這個推動力的初始化和更新是必須要做的,否則英雄會因為彈簧機制一直被拉回身體附近而停止不動。
一直往外跑擋不住不知道什么原因
game_sim_region.cpp:引入traversable_search_flag,并為GetClosestTraversable增加Unoccupied標記
當我們調用 get_closest_traversable
來為實體找到最近可行走區域并將其放置時,我們可能需要加入一個“未被占用”的標志,以確保實體不會被放置到已被其他對象占用的位置。
為此,我們會在 sim_region
中調用 get_closest_traversable
時傳入一個新的搜索標志,比如 TRAVERSABLE_SEARCH_UNOCCUPIED = 0x1
,用來表示搜索過程中只考慮未被占用的位置。
接著在 get_closest_traversable
的實現中,就可以根據是否設置了這個標志,來決定是否跳過那些已經被占用的可行走點,從而避免實體被放在不合適的位置上。這個機制為實體初始化、復活、克隆等功能提供了更可靠的落點邏輯。
我們實現了一個機制,允許在尋找最近的可行走位置時排除已被占用的位置。這一過程通過引入新的搜索標志 TRAVERSABLE_SEARCH_UNOCCUPIED
來完成。
我們在調用 get_closest_traversable
時,傳入了一個包含該標志的標志位集,用以控制搜索行為。在實際的搜索邏輯中,當我們對候選位置 P
進行檢查時,若標志中包含了 TRAVERSABLE_SEARCH_UNOCCUPIED
,我們就額外檢查這個位置的 occupier
字段。如果該字段為非空,表示該位置已被其他實體占用,則跳過此位置。否則將其作為合法的候選位置。
這樣一來,系統可以確保在將實體(例如玩家)放置到世界中的時候,不會把它放在已經有其他實體的位置上,避免重疊或邏輯錯誤。
此外,我們也不再需要強制將新玩家放置在攝像機當前的 P 點,而是通過上述邏輯選取一個最近且未被占用的合理落點,提升了放置邏輯的智能化和魯棒性。
在調試過程中,也提到了一些代碼格式相關的困擾,比如自動縮進錯位問題,影響可讀性,但這并不妨礙邏輯本身的推進。整體來看,這種基于標志的靈活搜索機制增強了系統處理實體放置的能力。
運行游戲,生成一排英雄
有Bug
我們現在需要測試剛剛實現的代碼。從理論上講,我們現在應該可以把新玩家放在已有玩家的身上,也就是說可以精確地放置在一個未被占用的、最近的可通行點上。
我們實際嘗試這么做的時候,確實發生了一些事情,玩家確實被放了下來,但視覺上看起來非常詭異。有點可怕的是,看上去像實現成功了,但又好像沒完全成功。新玩家雖然確實找到了放置的位置,但他們并沒有真正被放置在那個點上。這說明放置邏輯可能存在問題。
于是我們開始檢查 add_player
的具體邏輯。發現這個函數并沒有將玩家實體真正添加到世界上的某個實際位置上。雖然找到了落點,但代碼沒有把實體的實際坐標更新為那個位置。
由此我們確定:add_player
在位置處理上有缺陷。我們需要在添加玩家實體的時候,確保它被正確地放在我們已經計算好的那個落點位置上。當前的做法只是把它加入了世界實體列表,但沒有給出它的確切空間坐標。
接下來,我們需要修復 add_player
的實現,使其不光找到可放置的位置,而且要真正把實體的初始位置設置為這個點,確保它正確地出現在世界中的那個位置上。只有這樣,整個系統才會如預期般正常運作。
game_world_mode.cpp:讓AddPlayer函數接收SimRegion以便調用GetClosestTraversable
我們現在的目標是實現一個功能:能夠將玩家實體放置在我們當前周圍的可通行位置上。為了實現這個目標,我們正在處理與模擬區域(sim region)和世界坐標相關的邏輯。
在這個過程中,我們意識到一個問題:調用 add_player
時,需要提供的是世界坐標下的位置,但我們當前找到的可通行點是模擬區域內部的位置。這就需要一個從模擬區域坐標到世界坐標的轉換。
我們的思路是利用已有的模擬區域原點(sim_region_origin
)來完成這種坐標映射。這個原點本質上定義了模擬區域在整個世界空間中的位置。所以我們可以用 map_into_chunk_space()
或類似邏輯將一個模擬區域內的相對坐標轉換為世界坐標。
我們嘗試的方案是:
- 獲取模擬區域的原點。
- 調用
get_sim_space_traversable_standing_on()
這類方法獲取玩家可以站立的點。 - 使用轉換函數將這個相對點變為世界坐標。
- 最后將轉換后的坐標作為參數傳遞給
add_player
,實現正確的實體放置。
雖然整個邏輯有些復雜,但核心思路是清晰的。關鍵點在于:
- 轉換邏輯的正確性:模擬區域內的點需要加上
sim_region_origin
才能成為世界坐標。 add_player
要接受并使用世界坐標進行實體初始化。- 模擬區域在添加玩家時是活躍的,所以我們始終有對應的
sim_region
可用。
最后,我們初步實現了這一邏輯,并測試了轉換過程,雖然處理得較快,但整體方案是合理的,也確實能解決我們想要的問題:讓玩家出現在正確的位置上。這個處理雖然臨時完成得比較快,但效果看起來是可接受的。
運行游戲,發現問題仍未完全解決
我們當前實現的邏輯仍然存在問題,盡管我們找到了一個可通行的位置,并進行了坐標轉換,但實際添加玩家實體時,它依然是被放置在攝像機位置(cameraP
)而不是我們期望的目標位置。這是因為 add_player
調用時傳入的位置參數仍是 cameraP
,而非我們計算出的實體位置。
我們需要進一步修正調用流程,使其使用正確的世界坐標點。接下來,我們的思路如下:
- 不再使用
cameraP
作為添加實體的位置:這只是一個臨時的位置標記,無法反映我們想要放置實體的具體點。 - 使用實體位置替代
cameraP
:我們應該從目標實體或交互點(如跳躍點、可通行區域)中獲取位置,并將其作為玩家生成點。 - 修改
add_player
的調用參數:將位置參數從cameraP
改為實體或計算得到的坐標。 - 清理冗余代碼:例如此前定義的
*con_hero = something
其實沒必要,可以刪掉,避免混淆邏輯。 - 必要時臨時添加飛行變量:為了簡化調試或過渡測試階段,可以考慮加入一個“飛行狀態”或某種控制標記,以便讓新實體暫時具備特定行為或狀態,便于定位問題。
總之,這里核心的問題是調用添加玩家實體的邏輯時,位置參數沒有被正確替換。下一步我們會調整代碼,讓它基于目標實體或位置點來生成實體,而不是簡單地以 cameraP
為中心放置。這個修改會使玩家生成邏輯更準確、合理。
game.h:為controlled_hero添加DebugSpawn以便測試添加功能
我們正在實現一個調試生成系統,目的是在特定條件下生成玩家實體。具體操作流程如下:
我們將一個標志位 debug_spawn
設置為 true
,表示需要執行調試生成。我們把這段邏輯集成到處理受控英雄的區域,并在檢測到 debug_spawn
被設置時,執行我們自定義的“生成邏輯”。
我們修改了玩家生成的位置,不再直接使用 cameraP
,而是傳入我們自己計算的坐標 vp
,用于生成玩家。然而,如果忘記重置 debug_spawn
,就會導致玩家不斷生成,因此我們在生成完成后及時清除該標志,避免無限生成的問題。
目前主要的問題出現在“選擇可通行區域”這一步。我們想確保新的實體不會生成在已經被占用的位置上,因此加入了一個 traversable_search_unoccupied
標志,要求 get_closest_traversable
在查找時排除那些已經被其他實體占據的位置。
在實現過程中,我們邏輯是:
if (!(flags & TRAVERSABLE_SEARCH_UNOCCUPIED) || P->occupier == 0) {// 可以選擇這個點
}
這個條件的本意是:如果沒有要求“非占用”,那么直接允許;如果有要求,就必須確保該點沒有占用者。然而這個判斷似乎沒有生效,因為我們期望其排除有占用者的點,但實際上并沒有成功。
我們做了一些調試,例如嘗試移除標志以驗證是否和標志有關,結果發現并非標志本身的問題,而是 P->occupier
判斷邏輯似乎并未如預期那樣工作。這說明也許我們拿到的是結構的拷貝,或者沒有正確訪問原始數據導致 occupier
狀態不正確。
當前結論如下:
debug_spawn
機制基本正常,可控制實體生成。- 坐標傳遞和替換也基本正確,使用了我們計算的坐標
vp
。 get_closest_traversable
中occupier
判斷邏輯似乎存在問題,可能是數據引用或結構處理導致判斷失效。- 為避免無限生成,我們在生成后清除了
debug_spawn
標志。 - 接下來需要繼續檢查占用判斷處的邏輯,確認是否拿到的是正確的可通行點狀態。
整體邏輯已逐步清晰,離正確完成僅差一步排查 occupier
的問題。
game_sim_region.cpp:讓GetSimSpaceTraversable復制Occupier信息
我們意識到一個關鍵問題出在數據的傳遞方式上。在處理“尋找最近可通行點”時,我們之前是按值返回這個點(by value),而不是引用(by reference)。由于是值傳遞,這導致某些字段(尤其是 occupier
)在返回后就丟失了原始的真實信息,因為它沒有被正確地復制或者更新。
這個問題的根源是我們需要這個點的數據能夠包含當前占用者的信息(occupier
),用于判斷該位置是否空閑。然而,當前結構中我們并沒有在復制時將 occupier
字段一并復制過來,導致判斷 occupier == 0
永遠為真或者行為不一致,從而無法正確過濾掉已被占用的位置。
我們考慮過是否可以始終用引用的方式返回數據,但由于當前處理的是空間點(可能需要做空間轉換或復制),我們確實無法直接寫回原始位置,因此值傳遞在某些場景下是必須的。盡管如此,我們還是可以接受這種做法,只要確保在值傳遞時,將所有關鍵字段(包括 occupier
)都一并復制進去即可。
因此我們決定接受當前這種按值返回的方式,但需要顯式地復制 occupier 字段,確保后續的邏輯判斷能夠生效。這樣一來,get_closest_traversable
返回的點就能正確反映是否被占用了,篩選邏輯也就能正常工作了。
總結關鍵調整點如下:
- 目前使用的是值傳遞返回空間點;
- 原實現中未復制
occupier
字段,導致邏輯判斷失效; - 解決方法是明確復制
occupier
到返回的結構體中; - 盡管引用傳遞可能更高效,但在當前上下文中不可行;
- 只要確保關鍵字段完整復制,按值返回也是可以接受的。
這一步完成后,整體邏輯就可以正常根據占用情況判斷放置位置了。
運行游戲,生成一些正確定位的英雄
好的,現在一切已經就緒,可以完成收尾工作。
我們已經正確實現了查找“最近未被占用的可通行點”的邏輯。通過確保在返回該點的數據結構時,將 occupier
字段也一并復制進來,系統現在可以準確判斷某個點是否已經被實體占用。這個修復確保了我們不會將玩家放置到已有實體占據的位置上。
我們之前所做的準備工作如下:
- 添加了一個新的標志位
traversable_search_unoccupied
,用于控制是否只尋找未被占用的可通行點。 - 在
get_closest_traversable
中,根據是否設置該標志來判斷是否排除已被占用的點。 - 修復了由于按值返回結構體而導致的
occupier
字段未正確復制的問題。 - 在添加玩家時不再簡單地以
cameraP
為基準點放置,而是通過查找最近未被占用的可通行點,轉換為世界坐標后,作為玩家的實際放置位置。 - 增加了一個調試變量
debug_spawn
,用于控制是否在特定條件下觸發玩家生成邏輯,并確保該變量在生成后被清除,防止持續生成。
至此,玩家生成系統變得更健壯、更智能,能夠自動選擇一個安全、未被占用的位置進行放置,提升了系統在復雜場景下的適應能力,也為后續的擴展留下了良好基礎。下一步,我們可以考慮進一步測試不同邊界情況,例如多個玩家同時生成,或者嘗試生成在狹小空間中等,來驗證這個系統的穩定性和魯棒性。
game_world_mode.cpp:讓DebugSpawn不覆蓋已有設置
在 Handmade World 模式中,我們在進行調試生成(debug spawn)時,現在做出一個調整:不再讓新生成的實體替代當前實體,而是直接從當前實體的位置生成一個新的角色。這意味著當前的實體保持不變,我們只是從它的位置“派生”出一個新的實體。
這種做法有幾個優點:
- 不覆蓋已有實體:原本的實體不再被替換,避免了意外丟失或狀態中斷的問題。
- 明確角色生成機制:清晰地區分了“生成新角色”和“接管現有角色”的邏輯路徑,使系統行為更可預測。
- 更方便調試:我們可以多次從同一個位置生成多個實體,便于觀察行為、驗證碰撞處理、可通行點查找等邏輯。
- 保持原始控制狀態:原實體的控制狀態不變,新生成的實體默認不受控制,除非手動指定控制器。
通過這個改動,調試模式下的行為變得更符合直覺,同時也降低了調試過程中對現有游戲狀態的干擾,便于開發和問題定位。我們接下來可以繼續驗證此方式下生成實體的完整性,例如是否正確落地、物理狀態是否一致、是否避開已被占用的點等。
最后研究一下為什么沒有碰撞
和樹碰撞斷點一直沒進來