倉庫:https://gitee.com/mrxiao_com/2d_game_3
回顧 game_asset.cpp
的創建
在開發過程中,不使用任何現成的游戲引擎或第三方庫,而是直接基于 Windows 進行開發,因為 Windows 目前仍然是游戲的標準平臺,因此首先在這個環境中進行教學。然而,代碼是以高度可移植的方式編寫的,社區的許多成員已經成功將其移植到 Linux 和 Mac 等系統上,甚至有人讓它在 PlayStation VR 上運行,這一過程非常有趣。因此,在開發過程中始終保持代碼的可移植性,以便最終能輕松將游戲移植到多個平臺,這也是目標之一。
目前正在進行的是對游戲中資源(資產,assets)的擴展,包括如何存儲資源、如何處理資源,以及如何對資源進行標記,以便最終能夠創建一個資產包文件(pack file)。資產包文件的設計目標是讓資源管理更加高效,使得能夠批量導入資源,而不必手動逐個管理。在這一過程中,需要定義資源的組織方式、資源的查找機制,以及如何讓整個資源系統更加靈活易用。
現在處于這一探索過程的后期階段,預計到下周末之前,可以完成所有相關的探索工作,并開始對系統的細節進行最終的確定。目前仍然在做一些實驗,因為尚未完全確定資源系統的所有需求,因此需要不斷測試如何更高效地從資源系統中獲取資源,以及如何管理資源,以確保初步設計是合理的。只有在確保合理之后,才會正式確定資產包文件的具體格式。
回顧昨天的進展,創建了一個新文件,雖然在 Game Hero 的開發過程中很少創建新文件,但為了更好地組織和管理資源相關的代碼,決定新建一個文件來專門處理臨時資源工作,以便更專注地優化資源系統,并將其轉變為更加健壯的正式定義。目前一切運行良好,不過由于日常事務繁多,暫時不太記得昨天具體進行到哪里了。
回憶一下,昨天最后完成的工作是臨時實現了一種方式,使得可以從指定的資產 ID 中獲取第一個位圖(bitmap)。這樣可以測試資產表是否正常工作,測試結果表明它們運行得很好。因此,接下來的任務是進一步擴展這一概念。
目前有兩個可能的方向:
- 處理數組(array)資源的管理
- 處理命名資源(named elements),即包含多個部分的復雜資源
首先,決定優先處理數組資源的管理。至于原因,暫時不太確定,但直覺上似乎可以用一種更直接的方式來處理這兩種情況,而不需要分別進行特殊處理。因此,先從較簡單的部分開始,也許能夠找到一個通用的解決方案來處理所有類型的資源。
讓數組化的資源通過資源系統
接下來將從數組化的資源管理入手,并讓這些數組化的資源直接通過資產系統處理,而不是像現在這樣單獨提取出來。當前的實現方式是直接從資源數據中提取數組化的資源,而目標是讓它們通過資產系統進行管理,以便更好地組織和優化資源加載流程。
為此,首先需要弄清楚如何實現這一點。回顧現有的資產類型,已經具備了管理多個資源的概念,因此理論上不需要對系統進行太多調整來支持多個資源的管理。只需要將這些資源正確地添加到資產表中,使其能夠通過資產系統進行訪問和管理。
目前的資源包括:
- 資產 Grass(草地)
- 資產 Tuft(草叢)
- 資產 Stone(石頭)
接下來,需要在資產系統的其他部分,將這些資源通過系統加載進來,而不是單獨處理。可以開始嘗試通過資產表將它們整合進系統,以確保所有資源都能統一通過資產系統進行訪問和管理。
提供穩定的方式來定義 AlignX
和 TopDownAlignY
以及位圖對齊
在正式將數組化的資源管理集成到資產系統之前,有一個更基礎的調整可以先進行,以便后續操作更加順利。目前的代碼仍然使用硬編碼的方式來加載位圖資源,依賴 switch
語句進行選擇,而最終版本顯然不希望繼續使用這種方式。因此,下一步的目標是將這部分代碼重構,使其更加穩定和可擴展。
當前的資源加載過程中,位圖需要定義 AlignX
(水平對齊)和 TopDownAlignY
(垂直對齊),這些信息是必要的。為了讓整個系統更加統一,需要替換掉這些固定的對齊方式,使用一個更通用的對齊方案。現有的代碼存在一定的割裂性,比如有的資源對齊方式是 AlignPercentage = {0.5, 0.5}
,而有的資源使用其他方法進行對齊。之前這樣做的原因是,默認希望位圖的錨點在中心,而計算中心點的前提是需要先加載位圖。但現在隨著資產包系統的構建,這種方式需要調整。
新的資產包系統將允許在打包資源時提前計算并存儲對齊信息,而不需要在加載時再進行額外計算。資產包的構建流程會包含一個資源處理管線,在打包時就可以預先定義 AlignPercentage
,這樣在運行時只需要直接讀取這些預計算的對齊信息即可。
在 RenderGroup
相關代碼中,位圖數據實際需要的僅僅是 AlignPercentage
,因此可以直接在資源加載時指定 AlignPercentage
,徹底移除基于位圖大小計算對齊的邏輯,避免額外的依賴關系。
為了完成這項調整,首先需要確定現有位圖的對齊參數。最直接的方法是利用調試器,讀取當前加載的資源的對齊值。由于某些位圖的默認對齊已經是 {0.5, 0.5}
,因此重點需要關注的是那些使用特殊對齊方式的資源。
在進行調試時,需要先運行游戲,確保所有資源都被正確加載,然后再讀取所有位圖的對齊參數。這樣可以確保所有數據都已經就緒,便于在后續代碼調整時使用這些參數進行替換,從而完成 AlignX
和 TopDownAlignY
的移除工作。
移除多余的資源類型
為了確保所有位圖資源正確加載,還需要在最后額外調用 LoadAsset
來加載所有資源,避免遺漏。因此,需要為每個位圖資源調用 LoadAsset
,例如:Asset_Tree
、Asset_Rock
、 和 Test
等。然而,檢查代碼時發現 TestBackground
資源已經不再使用,因此可以直接移除。此外,還發現 Asset_Stairwell
和 Asset_TestBackground
這兩個資源似乎也未被使用,因此也可以清理掉。
最終,當前仍在使用的資源包括 AssetTree
和 AssetSword
,其他的可能已經被移除或者未被引用。為了確保加載邏輯的正確性,需要確認 AssetSword
是否仍然在代碼中引用。
在完成所有資源的加載后,可以查看它們的具體對齊參數。由于這些資源數量較少,可以直接逐個檢查對齊值,并將其復制到代碼中作為標準設定。這樣,就可以在后續的資產管理系統中統一使用這些參數,確保對齊方式的一致性。
在調試器中讀取位圖對齊信息
現在需要對資產進行檢查,確保所有位圖正確加載。當前有 8 個位圖資源,因此需要逐個查看它們的加載情況。
在檢查過程中,發現其中一個位圖未被加載,但這是 Asset_None
,即表示“無資產”的占位符,符合預期。除此之外,所有其他位圖均已成功加載,包括 Bitmap1
、Bitmap2
和 Bitmap3
。這些位圖分別對應于 Shadow
、Tree
和 Sword
資源,與定義的資產名稱相匹配,說明資源加載過程是正確的。
下一步可以繼續調整這些資產的對齊參數,并確保所有資產都能通過新的系統正確訪問和使用。
激活Sword 發現有段錯誤
用 AlignPercentage
替換 AlignX
和 TopDownAlignY
現在可以為這些位圖資源設置對齊參數(Align Percentage)。這些參數將直接從實際位圖數據中提取,并用于確保正確的渲染位置。
首先,獲取 Shadow
位圖的對齊參數,其 X 軸對齊值為 0.5
,而 Y 軸對齊值是一個較大的數值。這是合理的,因為陰影可能需要特殊的對齊方式。無論這個數值多么特殊,都不會影響整體處理。
然后,獲取第二個位圖(Tree
)的對齊參數,并將其復制到合適的位置。之后,可以移除臨時代碼,因為這些數據已經被正確應用。
最后,提取 Sword
位圖的對齊參數,并確保它與前面的數據格式一致。這是最后一個需要調整的對齊參數。
完成這些對齊參數的調整后,所有位圖的 Align Percentage
設定就完成了,確保了它們在渲染時能夠按照正確的位置進行對齊和顯示。
移除 DEBUGLoadBMP
并修改之前的 DEBUGLoadBMP
以適應 AlignPercentage
變更
現在所有位圖的對齊參數(Align Percentage)都已設置完畢。
接下來,可以移除之前的舊代碼,因為現在已經有了標準的 Align Percentage
設定。不再需要手動計算對齊方式,只需使用 Align Percentage
即可。
一旦 Align Percentage
設置完成,之前的特殊對齊處理代碼可以完全移除。例如,先前的 TopDownAlign
相關邏輯就可以刪除,不再需要進行特殊的 Y 軸調整。
在 Align Percentage
設定后,上層代碼可以直接使用它,并且 Align Percentage
的默認值可以設為 0.5, 0.5
,以保證默認情況下圖像以中心點為對齊基準。
所有位圖的 Align Percentage
現在都可以直接賦值,不需要再進行額外的計算或調整,從而簡化了整體代碼邏輯。
刪除 TopDownAlign
現在可以移除 TopDownAlign
相關的代碼,因為 Align Percentage
已經統一處理對齊方式。
在移除 TopDownAlign
之后,重新編譯時發現仍然有部分代碼依賴 TopDownAlign
,所以不能直接刪除。因此,需要先調整 Bitmap Work
相關代碼,將 HasAlignment
也一并移除,因為 Align Percentage
現在已經可以完全替代 HasAlignment
的功能。
調整 BitmapWork
代碼后,可以清理 HasAlignment
相關的邏輯,并直接使用 Align Percentage
進行位圖的對齊計算。同時,可以在適當的位置直接給 Align Percentage
賦默認值,因為現在對齊參數已經完全確定,不需要額外的判斷邏輯。
清理 HasAlignment
相關的代碼后,整體代碼結構變得更加簡潔,所有位圖的默認對齊方式也變得一致,提高了代碼的可讀性和維護性。
運行游戲
代碼中原先存在一些不夠整潔的部分,這些部分的實現方式較為零散,因此需要進行優化和整理。通過識別這些代碼區域并優化,使其更加簡潔高效,同時也更易于維護和擴展。
經過調整后,整體代碼變得更加經濟合理,不再有冗余的邏輯,所有對齊方式也已經正確應用。現在 Align Percentage
統一處理對齊計算,移除了 TopDownAlign
和 HasAlignment
,大幅減少了額外的判斷和復雜性。
最終,調整后的代碼保持了正確性,所有的對齊計算均按照預期工作,整體邏輯更加清晰,便于后續進一步擴展和優化。
在沒有打包文件的情況下,考慮如何清理當前代碼
接下來需要進行的優化是消除 test_hero_shadow.bmp
和 align_percentage
相關的代碼,使其更加簡潔。不過,目前由于 pak
文件尚未實現,因此短期內可能無法徹底優化,只能進行一些過渡性的調整。
在 pak
文件最終實現后,文件名等信息將被移除,因此現在的處理方式只是臨時方案。盡管如此,仍然可以先引入一個 bitmap_table
作為過渡,使得后續優化更加順暢。
整體而言,雖然目前的調整仍然受到文件名管理的影響,但這不會是一個長期問題。在現有結構下,逐步優化代碼的可讀性和清晰度仍然是有意義的。
修改 game_asset.h
中 asset_bitmap_info
結構
目前的代碼設計中,有一個 asset_bitmap_info
表示加載的位圖信息。為了暫時應對當前的需求,決定在代碼中模擬出一個位圖信息表。這張表與最終將會存在于 pak
文件中的版本略有不同,但目的是為了在當前階段能使用相似的數據結構。
每個位圖都包含位圖信息,且數量相同。雖然可以將這些信息直接存儲在 bitmap
結構或 assets slot
結構中,但目前并不打算這樣做,因為還不確定這些數據是會一直加載在內存中,還是會按塊加載出來。因此,決定將位圖信息表作為一個單獨的數組,并且使其與其他數據結構并行,以便保持靈活性。
這種設計方式能夠確保代碼在處理文件數據時更具靈活性,同時避免在不確定數據處理方式的情況下將其合并到其他結構中。
分配大量位圖進行測試
目前的代碼是為了測試目的,計劃先手動創建和分配大量的位圖。目的是為了確保能夠處理大量的位圖數據。雖然最終這段代碼會被替換掉,數據將從磁盤加載,但目前的測試代碼不關心實際的位圖數量和加載方式,只是為了確保當前能處理和分配大量位圖。
引入 DEBUGAddBitmapInfo
接下來,創建了一個調試用的函數 DEBUGAddBitmapInfo
,用于模擬資產打包器的功能。這個函數會初始化一個名為 DEBUGUsedBitmapCount
的變量,初始值為零,用來模擬在打包過程中每次添加一個位圖時的計數。
每次添加位圖時,DEBUGAddBitmapInfo
會接收文件名和對齊百分比等信息,然后將這些信息用來構建位圖的相關信息。位圖的相關信息會存儲在一個結構體中,包含了文件名和對齊百分比等必要數據,最終返回一個 ID,用來標識這個添加的位圖。
該過程還包括一些檢查,比如確保添加的位圖數量不超過設定的最大位圖數量。這些代碼純粹用于測試,因此不需要考慮實際的位圖支持數量,最終會通過從包文件中加載數據來確定實際所需的位圖數量。
在 AllocateGameAssets
中動態創建資源
現在,我們可以開始將資產動態地創建,模擬打包器的工作流程。為此,首先要做的是將資產的計數與位圖的添加過程關聯起來。具體來說,創建一個新的函數,該函數會在每次調用時,動態地處理資產的添加。
此過程會基于已有的資產,比如樹的陰影和劍的位圖,進行模擬。我們通過增加一個新的變量來追蹤資產數量,這個數量應該與實際的資產計數一致。通過這種方式,系統可以根據實際的需要動態增加資產。
接下來,通過引入一個類似 AddBitmapAsset
的函數,每次添加位圖時,我們都會傳入位圖的詳細信息。這些信息會被傳遞給每個資產,確保每個資產能夠正確地添加到系統中。通過這種方式,模擬過程會像實際從包文件加載資產一樣,并且支持多次調用。
這個方法最終會使得每次調用 BeginAssetType
時,都能夠添加一個新的資產并且更新相關的計數和結構,直到所有的資產都被正確加載和處理。這也為未來的資源打包過程提供了清晰的基礎,盡管現在只是模擬這一過程。
提取 BeginAssetType
、AddBitmapAsset
和 EndAssetType
為函數
現在,我需要定義一些函數來實現當前的功能。當前我們使用的調試方法非常有效,所以我決定繼續沿用這種方法,直到我們能夠加載一個實際的資產包文件。當包文件加載完成后,這些調試代碼就可以去掉。
接下來,我的目標是使這些函數能夠執行當前的操作。我們已經定義了一些函數,像 BeginAssetType
,這些已經準備好了。接著,我們要確保在這些函數中傳遞正確的參數,尤其是文件名和對齊百分比的順序,確保它們的一致性,避免混亂。
在接下來的實現過程中,我將把現有代碼分解成多個步驟。首先,我們需要確認類型ID,確定資產的類型,并且要實現一個斷言檢查,確保在開始一個新類型之前,沒有其他線程正在執行同樣的操作。這樣可以避免并發問題。
另外,有一點要特別注意,我之前花了時間去將某些變量清零,認為它們需要初始化為零,但實際上這些變量在程序中總是被初始化為零。所以我意識到我之前做的這一步是多余的,應該停止這種做法。這只是一個無意間做出的習慣性操作,實際上并不需要這樣處理。
總的來說,這些代碼的核心目的是為了模擬資產加載和管理的過程,并確保在真實的資產包文件加載之前,調試過程能正常運行,避免后續出現問題。
繼續編寫這些函數
在這段代碼中,我主要在處理資產類型和調試相關的功能。首先,我計劃在結束資產類型時進行一些斷言,確保它的初始值為零,并且在結束時對其進行清理,從而可以在后續繼續使用。接著,我注意到一個錯誤,資產類型不應該與位圖計數相同。資產類型應該與調試用的資產計數相對應,因此我進行了相應的修正。
在初始化時,我將資產類型設置為調試用的資產計數,并且將其索引初始化為零,這意味著當前還沒有實際的資產,但它已經做好了添加的準備。然后,在添加位圖資產時,我們會確保獲取并正確使用資產類型,同時更新調試用的資產計數。
在結束時,雖然沒有特別的操作,但我們會確保在添加資產時,通過正確的索引和類型進行更新。同時,還需要設置正確的槽ID,這個槽ID指向的是添加的位圖資源,以便于后續正確使用。
總的來說,整個流程確保了資產和位圖信息的正確管理,盡管目前處于調試階段,但這個方法可以模擬最終在加載資源包時的操作。
在調試器中單步執行這些函數
我們意識到這個過程有點復雜,所以即使代碼里沒有bug,我們也希望一步步地檢查它,以便更清楚地了解它的運作方式。因此,我們決定在“AllocateGameAssets”(分配游戲資源)這個地方設置一個斷點,來觀察具體發生了什么。我們打開代碼,開始逐步分析這個情況。為了更清楚地看到資源結構,我們先把調用棧的窗口縮小一點,這樣能更好地查看資源相關的部分。
我們看到這里有幾個關鍵的東西在起作用。首先,我們有一個“AssetTypes”(資源類型數組),用來存儲不同類型的資源信息。然后,我們還有一個“Assets”(資源數組),用來存放具體的資源數據。此外,我們有兩個與位圖相關的數組:“Bitmaps”(位圖數組)和“Bitmapinfos”(位圖信息數組)。位圖信息數組是我們正在填充的內容,而位圖數組則是實際加載位圖數據的地方。
接下來,我們開始檢查這個流程。我們從資源類型數組中取出一個對應的資源類型,比如“shadow”(陰影),它的索引是1。我們獲取這個資源類型時,會用到一個“DEBUGUsedBitmapCount”(已使用的資源計數),一開始這個計數是0。不過,我們覺得應該為它預留一個槽位(slot)。我們認為從0開始計數可能不太合適,所以決定讓計數從1開始,把0號槽位留空作為一個空槽(null slot)。這樣做成本不高,而且會讓后續管理更簡單。于是,我們調整了代碼,把“DEBUGUsedBitmapCount”(位圖計數)和“DEBUGUsedAssetCount”(資源計數)設置為從1開始,0號槽位就不占用。
調整之后,我們重新審視流程。我們從資源類型數組中取出陰影對應的資源類型,把它的“FirstAssetIndex”(第一個資源)的索引設置為當前的“DEBUGUsedAssetCount”(調試用的資源計數),也就是1。我們看到代碼里把這個值設置好了,同時把“OnePastLastAssetIndex”(最后一個資源)也設為1。這表示當前還沒有其他資源,但如果有的話,1號就是第一個資源。
然后,我們嘗試添加一個位圖資源。我們先取出資源數組中當前正在處理的最后一個資源的索引,把新的資源追加到后面,并把索引遞增。我們為這個新資源設置了一些標簽(tags),這個資源實際上是資源數組里的第1號資源(asset1)。它的初始值是0,我們清空了一些匹配信息(matches),以確保干凈的狀態。接著,我們為調試用的位圖信息數組分配一個槽位ID(slot id),并在這個位圖信息數組中添加一個新的位圖信息。因為0號槽位被預留了,所以這個新位圖信息被添加到1號位置。
我們繼續看代碼,取出了剛剛分配的位圖信息的ID,設置了它的文件名(FileName)和對齊百分比(align percentage),然后返回了這個ID。這個ID會被記錄下來,對應到剛剛設置的位圖信息。我們檢查下來,覺得這個過程看起來沒問題。
最后,我們處理“end assets”(結束資源分配)的部分。這一步主要是完成整個流程。我們確保“debug asset count”(調試資源計數)至少等于我們添加的資源數量,然后把資源類型的計數重置為0,表示這個類型的資源處理完畢。
整個過程就是這樣,我們通過逐步檢查,確認了資源分配的邏輯是合理的,而且通過預留0號槽位讓代碼更清晰。現在我們覺得這個實現應該能正常工作。
運行游戲
現在可以運行程序了。經過測試,程序運行正常,我們的目標已經實現,場景中已經成功加載了小羊和小樹,正如我們所期望的那樣,一切重新加載工作正常。
接下來,準備繼續推動工作進展,利用現在已經具備的功能,繼續進行后續的開發和測試。
讓 Grass
、Tuft
和 Stone
位圖使用新系統
現在我們進行下一步工作,目標是將草地(tufts)加入到系統中。我打算開始一個資產類型,并訪問未處理的草地資產類型(grass)。接著,我會用我們已經實現的系統來處理這個草地圖,給它命名并添加。
這一次,我會添加兩個草地圖,因為同一個資產類型下有兩種不同的草地圖,而不像之前的情況只涉及到一個草地圖。我會讓系統支持這個需求,并確保它能正常工作。
之后,我會將草地圖的處理從這里移除,并將其轉化為需要的格式,因為我們已經在資產中加入了草地資產。這將促使編譯器對函數進行正確處理。
在 game.cpp
中引入基于 ID 選擇資源的方法
接下來,我們將讓編譯器強制執行一些操作,并回到之前處理草地資產的地方。我們當時做了一個隨機選擇的操作,這次我們不會做特別復雜的處理,而是稍微簡單一些。具體來說,我會做以下幾點:
- 我先將石材相關的代碼注釋掉,然后將草地圖的部分改成使用隨機選擇的資產ID。
- 我打算從游戲資產中隨機選擇一個資產ID,具體來說,就是從所有草地圖資產中選擇一個。
- 為了實現這一點,我會使用一個隨機數生成器,它可以從游戲資產中選擇一個資產,這些資產必須符合草地類型的條件。
- 然后,繪制時,我們將使用基于ID的方式來渲染,而不再使用基于位圖的渲染方法。
通過這種方式,我們將從可用的草地資產中隨機選擇一個,并使用其ID進行渲染。
在 game_asset.cpp
引入 RandomAssetFrom
接下來,我們需要將隨機選擇的資產ID轉換為能夠加載資產的機制。這基本上類似于之前用來獲取第一個位圖的操作,但這次我們是根據隨機選擇的方式來獲取資產。
具體來說:
- 首先,我們將從資產類型中選擇一個隨機的資產。原本的做法是直接選擇第一個位圖,而現在我們要做的是從草地資產類型中隨機挑選一個。
- 為了實現這個功能,我決定使用一個隨機選擇的函數,并傳入合適的參數,這樣我們就能夠從草地資產的范圍內隨機選擇一個。
- 通過給定一個范圍,我能夠選擇一個隨機的值,在這個范圍內查找并返回對應的資產ID。
- 最后,我們會將選擇的隨機資產ID作為槽ID返回,并用于后續的操作。
通過這種方式,我們能夠在草地類型的資產中進行隨機選擇,并將其ID作為槽ID進行渲染。
忘記去掉之前的代碼了
運行游戲
在執行過程中,程序確實成功選擇了一個資產,但很明顯并不是正確的資產。當前的問題是,草地資產的部分竟然點亮了樹木,而不是正確的草地資產,這顯然是不對的。
需要進行調試,找出為什么會發生這樣的錯誤。盡管 bug 可能會讓人感到挫敗,但有時候它們也會帶來一些有趣和滑稽的情況。這次的錯誤屬于后者,看起來相當有趣,不過仍然需要修復它。
下一步的計劃是深入排查,為何隨機選擇的資產沒有正確落在草地資產范圍內。可能是索引計算錯誤,或者是隨機選擇邏輯的問題。通過設置斷點,并檢查調試信息,能夠更快找到問題的根源并進行修復。
在 RandomAssetFrom
中添加第一個資源
目前使用的隨機數生成方法 random.nextInt()
或 random.choice()
的范圍包括了最大值,這其實并不是我們想要的效果。所以考慮到這一點,決定保持現有的方式,但需要明確隨機數生成的范圍應該是 [min, max]
,且最大值會被包括在內。
為了調整這個問題,可能需要調整隨機數生成的范圍,確保它符合預期的邏輯,而不是導致錯誤的資產選擇。
在調試器中查看資源表
為了確保當前的邏輯沒有明顯的錯誤,我們決定先查看當前的表格數據,以便確認是否存在顯而易見的問題,而不是直接進行復雜的帶寬調試。
目前,我們已經確認 asset
這一部分的數據,其中草地 (grass
) 資產位于編號 5
的位置。我們按照索引 0, 1, 2, 3, 4, 5
進行了檢查,并確定草地的 FirstAssetIndex
是正確的。
接下來,我們需要確保所有相關的數據結構和索引計算邏輯沒有問題,以便正確處理 FirstAssetIndex
,并避免不必要的錯誤。
在 RandomAssetFrom
中添加第一個資源
我們已經發現了代碼中的一個錯誤,而且甚至不需要進一步單步調試就能確定問題所在。
當前的代碼邏輯中,我們需要確保仍然正確地添加第一個被隨機選中的資產,并且要保證這個選擇在正確的范圍內。我們正在使用 random between
這一函數來生成隨機值,而 random between
的實現方式如下:
min + random_next() % (max + 1 - min)
這一計算方式實際上是包含了 max
值的,而這并不是我們想要的行為。我們本希望 random between(min, max)
生成的值是 [min, max-1]
,但現在它的范圍是 [min, max]
,即 max
也可能被選中。
為了解決這個問題,我們可以保留當前的 random between
邏輯不變,但在使用時手動調整范圍,例如改為:
random between(min, max - 1)
這樣可以確保不會超出預期的索引范圍,并避免因 max
過大而導致訪問非法內存或錯誤映射資產。
運行游戲,注意到樹消失了
目前的情況是,場景中的樹完全消失了,這顯然是不對的。我們需要找出原因,弄清楚到底發生了什么。
首先,代碼運行后,我們觀察到樹的紋理沒有正確渲染出來,它們徹底從游戲畫面中消失了。這是一個比較奇怪的現象,因為按理來說,樹應該依然存在,代碼邏輯沒有涉及移除樹的部分。
目前的懷疑點在于資產管理系統的某個部分可能影響了樹的加載或渲染。我們最初認為可能是某個尚未實現的功能導致問題,但回頭檢查后,發現這個功能并沒有被啟用,因此可以排除這個可能性。
在 game.cpp
調查樹缺失的問題
首先,我們嘗試了一個新的調試方法:暫時移除地面塊的渲染,看看是否影響樹的顯示情況。結果表明,當不渲染地面塊時,樹就能夠正常顯示。這說明問題并不出在樹本身,而是與地面塊的渲染過程有關。
進一步分析發現,問題可能出在獲取隨機資產并推送到位圖緩沖區的過程中。原本的假設是,如果不進行 PushBitmap
操作,一切應該都會正常運行。然而,實際情況表明,執行 PushBitmap
之后,加載資產的邏輯被觸發,而這可能導致了樹的顯示問題。
具體來說,我們懷疑地面塊的渲染可能影響了樹的渲染流程,導致樹的位圖數據未正確加載或被覆蓋。以下是可能的原因:
- 地面塊的渲染邏輯影響了樹的繪制順序,可能導致樹的圖層被地面塊錯誤覆蓋。
PushBitmap
操作影響了資產加載,導致樹的紋理被錯誤地替換或移除。- 某些數據結構的錯誤修改,可能在執行
PushBitmap
時,導致樹的相關數據發生了變化。
下一步,我們需要深入檢查 PushBitmap
的實現,確定它是否影響了樹的渲染流程。可以嘗試:
- 手動檢查資產加載順序,確保樹的紋理仍然存在于資產列表中。
- 逐步執行
PushBitmap
操作,觀察哪一步導致樹的消失,以確定根本原因。 - 調整繪制順序,先渲染樹,再渲染地面,看看是否能夠恢復正常顯示。
總的來說,問題的核心似乎是地面塊的渲染與樹的渲染存在某種沖突,導致樹在某個階段被錯誤移除或覆蓋。
確定 LoadBitmap
是否是線程安全的
仔細回顧代碼后,我們開始思考一個新的問題:加載資產的過程是否是線程安全的?最初并沒有特別關注這個問題,因此需要重新檢查代碼,確保在多線程環境下不會導致競爭條件或數據損壞。
我們首先查找了 load asset
的調用情況,發現它在渲染過程中被調用。在分析 load asset
相關代碼時,注意到其中涉及 LoadBitmap
這一函數。進一步跟蹤調用鏈后,發現 LoadBitmap
可能是導致問題的關鍵。
為了確保多線程友好性,檢查了 LoadBitmap
的實現,發現其中調用了 PlatformAddEntry
來處理位圖加載。從代碼結構來看,這部分邏輯似乎已經做了線程安全處理,因此理論上不應該引發并發問題。
不過,為了進一步確認,我們需要考慮以下幾點:
- 是否有其他代碼路徑調用
LoadBitmap
而未進行線程同步? PlatformAddEntry
是否真正保證了線程安全?是否可能存在資源競爭?- 如果多個線程嘗試同時加載相同的資產,是否可能導致渲染異常?
接下來的調試重點是:
- 添加日志或斷點,檢查
LoadBitmap
在多線程環境下的行為,確認是否存在并發問題。 - 測試不同的加載順序,看是否影響渲染結果,尤其是地面塊與樹的渲染順序。
- 嘗試手動同步,如添加鎖或使用線程安全的數據結構,觀察是否改善問題。
目前的初步結論是:代碼已經嘗試了線程安全處理,但仍需進一步驗證,確保 LoadBitmap
在所有情況下都能正確運行。
使用 asset_bitmap_info
表
在回顧代碼后,發現一個關鍵問題:雖然之前已經確定了我們想要的位圖 ID,但并沒有真正存儲它。這意味著雖然所有邏輯都已經實現,但最終并沒有正確地應用所獲取的數據。
問題分析
- 我們確定了位圖 ID,但沒有真正將其存儲或使用。
- 代碼中仍然保留了對
AlignPercentage
和FileName
的引用,但實際上這些信息應該來自bitmap info table
,不再需要額外存儲。 - 忘記移除舊邏輯,導致無效數據仍在代碼中存在,從而影響了渲染流程。
修正方法
- 移除不必要的變量,例如
AlignPercentage
和FileName
,因為這些信息已經存儲在bitmap info table
中,不需要重復存儲。 - 直接使用
bitmap info table
來獲取位圖信息,而不是依賴舊的邏輯。 - 確保正確存儲并使用
bitmap ID
,以便后續可以正確地渲染對應的位圖。
反思
這次錯誤的本質是沒有完全清理舊代碼,導致邏輯雖然更新了,但仍然受舊代碼的影響,最終使得正確的數據沒有被使用。這提醒我們,在重構或優化代碼時,不僅要加入新邏輯,還要確保舊邏輯徹底清理,避免混亂和無效的計算。
重新啟用 PushBitmap
在 RandomAssetFrom
中使用 Choice
而非 Count
目前發現了一個邏輯錯誤,導致程序錯誤地訪問了無效內存區域。
問題分析
-
錯誤操作:錯誤地增加了
count
值- 代碼邏輯本來是從
group_assets
中隨機選擇一個索引choice
,然后使用這個索引來獲取SlotID
。 - 但是,在計算
SlotID
時,錯誤地加上了count
,這會導致索引超出數組范圍,進入未定義行為的區域(“no man’s land”)。
- 代碼邏輯本來是從
-
錯誤的后果
- 訪問數組時超出了合法范圍,可能導致:
- 讀取到未初始化的內存,數據錯誤。
- 發生非法訪問,導致程序崩潰。
- 這個錯誤讓之前所有的隨機選擇邏輯都變得無效。
- 訪問數組時超出了合法范圍,可能導致:
修正方案
-
去掉
count
偏移- 只使用
choice
直接索引group_assets
,而不是choice + count
。 - 確保
choice
的范圍是0
到count-1
之間,避免溢出問題。
- 只使用
-
增加邊界檢查
- 在使用
choice
時,添加assert(choice >= 0 && choice < count)
,確保索引合法。 - 在
group_assets
訪問時,確保數據結構中確實有足夠的元素。
- 在使用
-
添加調試信息
- 輸出
choice
和count
,以便檢查它們是否在合理范圍內:printf("Choice: %d, Count: %d\n", choice, count);
- 輸出
總結
之前的錯誤是因為在計算 SlotID
時,錯誤地增加了 count
,導致數組訪問超出范圍。修正方法是直接使用 choice
作為索引,同時增加邊界檢查,確保數據索引在合法范圍內,以防止出現未定義行為。
運行游戲,疑惑為何失敗
目前的問題似乎還沒有完全解決,盡管做了一些修正。具體來說,某些資產的 SlotID
始終為零,這導致了對這些資產的處理不正常。
問題分析
-
SlotID
為零的現象- 本來應該隨機從可用的資產中選擇一個,但現在某些資產的
SlotID
一直為零。 - 這意味著這些資產的選擇過程中存在錯誤,可能是計算方式不正確,或者數據訪問時發生了意外的行為。
- 本來應該隨機從可用的資產中選擇一個,但現在某些資產的
-
為什么會失敗
- 當
SlotID
為零時,程序會嘗試加載一個無效的位圖(bitmap)。這種情況下,load bitmap
會立即失敗,因為沒有有效的圖像數據。 - 但這并不解釋為什么所有的
get first bitmap id
調用都會失敗,或者為什么會影響到整個加載過程。
- 當
假設的原因
-
無效資產導致加載失敗
- 如果嘗試加載的資產無效(例如,
SlotID
為零),加載過程就會失敗。雖然理論上這應該不會影響隊列的其他操作,但可能存在某些資源或數據加載的機制相互影響,導致隊列未能正確處理。
- 如果嘗試加載的資產無效(例如,
-
隊列未被正確填充
- 當加載無效位圖時,可能導致隊列沒有按預期添加其他有效的加載任務。因此,雖然沒有數據被加載到隊列,但隊列本身可能在某種情況下未能正常工作。
-
其它潛在的線程問題
- 如果涉及到多線程操作,可能會有線程同步問題,導致某些任務被跳過或錯誤執行。
下一步行動
- 檢查
SlotID
的來源和計算過程,確保其值在合法范圍內,不會總是為零。 - 在加載位圖時,添加更多的調試信息,輸出加載失敗時的狀態,檢查是否存在無效資產導致的失敗。
- 測試隊列和線程機制,確保在加載失敗時,其他任務仍然能夠正常進行,并且隊列能夠正確管理和處理任務。
總結
目前的問題仍然與 SlotID
為零以及隨之而來的加載失敗有關,可能與無效數據或隊列機制相關。接下來需要對這些方面進行詳細的調試和測試,以確保程序能夠在各種情況下正確運行。
完成資源數組設置
首先,需要完成軟件的修復工作,以確保系統能夠正常運行,特別是在處理資產映射時。通過修復和調整相關的代碼,現在可以確保資產(例如石頭等)能夠正確加載并映射。這樣一來,程序就能正常運行,所有的功能也可以順利操作,確保這些資產在游戲中能夠正確顯示并發揮作用。
在 game.cpp
設置 Stamp
使其從 RandomAssetFrom
獲取
現在,程序可以正常運行,并且我們能夠正確地選擇隨機資產來進行渲染。通過使用隨機選擇,我們可以從草地數組中選擇一個隨機的資產作為“stamp”,否則就從石頭數組中選擇。這段代碼實現了隨機選擇,確保了程序能夠根據需要選擇草地或石頭的資產。接下來,“stamp”將會使用這些隨機選擇的資產,確保能夠渲染不同的草地或石頭類型。同時,程序中的每個步驟也都正確地通過隨機選擇方式進行處理,確保了功能正常。
運行游戲,注意地面塊缺失
現在,我們已經正確地翻譯了所有內容,但依然存在一個bug,問題可能出在沒有正確刷新某些數據,或者做了一些不該做的操作。雖然大部分功能正常,但這個問題仍然需要解決。即使這樣,有時會很幸運,第一次嘗試就能順利運行,但大多數時候還是需要調試和修復一些小問題。時間雖然所剩不多,但問題已經明確,接下來需要進一步檢查和修復,以確保一切正常工作。
在 game.cpp
編寫 AllResourcesPresent
的失敗情況處理
問題出在我們處理任務時出現了一個明顯的錯誤。當加載失敗且任務尚未完成時,我們錯誤地繼續保留了這個任務。實際上,我們應該在任務失敗時中止它,而不是讓任務繼續"偽運行"。這種情況導致任務堆積,從而無法加載任何新內容。關鍵問題在于任務沒有被正確中止或清除,造成了資源的浪費和后續無法加載其他內容的情況。所以,正確的做法是在失敗時立即終止任務,防止任務繼續占用資源。
在調試器中檢查 FillGroundChunk
我們正在檢查任務是否能正確結束并釋放內存。我們嘗試調用任務相關的內存清理功能,但機器報告說無法完成此操作。于是我們開始進行一些調試,嘗試通過設置斷點來觀察任務是否按預期執行,看看問題出在哪里。發現目前任務尚未正確設置,所以問題可能出在任務的初始化階段。
在 FillGroundChunk
頂部設置 RenderGroup
、Buffer
和 Task
我們發現了一個問題,之前沒有正確設置任務的相關信息。現在決定在渲染時正確設置這些信息,確保緩沖區任務可以被正確處理。實際上,在調用平臺的add entry
之前,這些信息并不會被檢查,因此現在決定在這之前就設置好它們。這樣做應該能夠避免之前的問題,確保任務可以順利執行,并且提升代碼的安全性,防止其他潛在的問題。
地面塊設置的有問題
修復
“你提到的‘先寫用法代碼’是什么意思?此外,Asset 0
是否有特殊處理?”
在編寫代碼時,應該先編寫使用該系統的代碼,而不是先編寫系統本身的 API 及其結構。先編寫使用代碼的目的是為了觀察用戶希望如何使用這個系統,然后再去實現該系統。這樣,整個開發過程會以實際使用需求為導向,而不是憑空想象一個接口的設計方式。
如果一開始就設計和實現系統的 API,可能會導致 API 的使用方式不夠合理,最終在代碼的各個使用場景中都會遇到不便或不直觀的問題。而如果先編寫使用代碼,就可以從實際需求出發,使 API 的設計更符合使用者的直覺,從而提高代碼的可讀性和易用性。
在軟件開發中,尤其是在游戲開發中,一個小的功能可能只需要實現一次,但在整個游戲代碼庫中可能會被成百上千次地調用。如果 API 設計不合理,開發者在每個使用場景中都會面臨額外的復雜性,而如果 API 設計合理,所有使用它的代碼都會變得更簡潔、易讀。因此,最佳做法是先編寫多個實際的使用示例,看看在不同場景下如何調用該功能,然后再設計實現該功能本身,這樣能確保 API 設計對絕大多數使用場景都是友好的。
至于對資產(asset)編號為 0 的特殊處理,目的是讓 0 代表“無資產”(nothing)。這樣,在代碼中提及某個資產時,可以用 0 來表示“沒有資產”,從而在邏輯上保持一致性。例如,在某個游戲邏輯中,需要指定玩家獲勝時顯示的資產,如果該值為 0,則意味著不需要顯示任何資產。這種處理方式可以使代碼更清晰,也更容易理解。
“你會不會故意降低游戲畫質,以制造爭議,讓游戲在 NeoGAF 和 Reddit 上被討論?”
在游戲開發和推廣過程中,可以通過制造爭議來吸引更多關注,因為“沒有壞的宣傳”。為了引發討論和熱議,可以采取多種方式來制造話題和爭議,其中之一就是故意降低游戲的畫質,以引發玩家的不滿和討論,讓游戲成為熱門話題,在社交平臺和論壇上引發廣泛關注。
除此之外,還可以通過其他方式進一步制造爭議。例如,可以接受某個廠商的資金支持,使游戲成為獨占作品,從而激怒其他平臺的玩家。比如,可以與某個主機廠商簽訂協議,推遲 PC 版的發行時間,以此引發 PC 玩家群體的不滿,讓他們在社交平臺上掀起討論。
此外,還可以在游戲內容上采取一些更具爭議性的做法,例如在角色選擇方面進行某種歧視性的設定,或者在游戲劇情、角色設定等方面故意制造爭議。所有這些做法的目的,都是為了在社交媒體、論壇和新聞報道中引發熱議,從而擴大游戲的曝光度,吸引更多的關注和討論。
可以整理一份詳細的“爭議制造策略”清單,列出所有可能引發討論和爭議的做法,并有計劃地實施,以最大程度地提高游戲的知名度和討論熱度。
“哪些游戲性改動會需要修改資源標簽?”
在討論游戲機制的改動時,需要考慮這些改動是否會影響到標簽(tags)的使用。然而,標簽的主要作用是描述游戲中的資源(assets),而不是直接反映游戲玩法的變化。因此,游戲機制的改動未必會直接導致標簽的變化,除非新的機制引入了需要額外描述的新資源。
如果游戲增加了一些新的玩法,而這些玩法涉及到新的資產描述方式,那么可能就需要新增或調整標簽。例如,如果游戲新增了一種特殊的交互方式,需要對某些資源進行特定的標記,以便正確調用或顯示,那么這就需要對標簽系統進行調整。但總體而言,游戲機制和標簽系統并不是完全緊密相關的概念,兩者的關聯性可能并不強。
“長時間調試 bug 會讓你犯困嗎?”
在調試代碼時,如果花費了很長時間解決一個錯誤,可能會感到困倦。但這種困倦感并不是因為錯誤本身難以解決,而是因為本身已經感到疲憊,導致解決問題的效率降低。
有時,由于困倦,思維變得遲緩,導致調試過程變得更加緩慢,解決問題的時間也因此被拉長。但并不能確定是因為調試時間過長才導致了困倦,更多時候,困倦本身就是影響解決問題效率的原因。
“你會在直播中展示資源管線工具的編程過程嗎?”
當前的重點仍然是游戲本身的編程,因此不會涉及資產(asset)流水線工具的展示。之前已經討論過,出于管理范圍的考慮,編程工作被限定在游戲開發本身,而資產流水線屬于完全不同的編程領域,與游戲代碼的核心部分關系不大,因此不會納入當前討論范圍。
目前的工作重點是制定數據文件格式的規范,這也是當前正在進行的任務之一。完成這一步之后,會單獨處理與該格式兼容的 pak 文件的生成,就像在實際開發過程中,資產管理流程通常由專門負責資產流水線的人員來完成一樣。資產文件的打包和管理將作為一個獨立的流程進行,而不會與當前的游戲代碼開發直接交叉。
“為什么要用 size_t
而不是 int32
或 uint32
?”
在編程中,使用 size_t
的主要原因是為了在不同的系統架構(如 32 位或 64 位)上正確表示內存大小,而不依賴具體的整數類型,如 int32_t
或 int64_t
。
int32_t
和 int64_t
分別代表 32 位和 64 位的整數類型,但它們并不隨平臺的不同而自動調整。因此,在編寫需要處理內存大小的代碼時,如果直接使用 int32_t
或 int64_t
,可能會導致跨平臺兼容性問題。例如,在 64 位系統上,使用 int32_t
可能無法正確存儲較大的地址或內存偏移量,而使用 int64_t
在 32 位系統上則可能會浪費存儲空間。
size_t
解決了這個問題。它的大小會根據平臺的架構自動調整,在 32 位系統上,它是 32 位的,在 64 位系統上,它是 64 位的。這樣,就可以確保它足夠大,能夠表示系統允許的最大內存大小。
使用 size_t
主要用于涉及內存分配、數組索引、指針運算等場景,因為它能夠保證變量的大小適應不同平臺的內存模型,避免因整數類型大小不同而引發的錯誤。
“你會支持 HTC Vive 和 Lighthouse 嗎?”
目前沒有計劃支持 HTC Vive 或其他類似的 VR 設備,主要原因是當前項目是一個 2D 游戲,而 VR 設備通常用于 3D 體驗。如果沒有明確的 3D 交互方式或視覺呈現方案,支持 VR 設備的意義并不大。
VR 設備的核心價值在于沉浸式的 3D 體驗,包括深度感知、空間交互以及頭部追蹤等功能。而對于 2D 游戲而言,這些功能并沒有明顯的應用場景。因此,在沒有清晰的 3D 設計目標的情況下,增加對 HTC Vive 等設備的支持并不合理,也無法充分發揮 VR 設備的優勢。
“如果不在直播中做資源管線,你還會公開源碼嗎?”
資產流水線并不會成為產品的一部分,實際上,它只是用于生成數據集,而這個數據集最終會成為游戲的一部分。至于如何生成這些數據集,實際上并沒有承諾它會在其他人的機器上編譯運行,因為這一部分的實現是相對獨立的,只是根據需要進行的操作,以確保生成的數據文件符合預定的格式和描述。
雖然生成數據集的過程可能并不會公開,但會有一個屏幕上的描述,任何有興趣的人都可以根據這個描述構建自己的 pack 文件。這并不是特別復雜,實際上就是將多個數據拼接在一起,制作文件的過程并不難。
不過,可能會使用一些專有的工具或代碼(比如自己編寫的 Photoshop 解析器),這些工具和代碼并不屬于公共領域,因此它們不會包含在發布的版本中,也不會公開給其他人使用。這些工具主要是為了生成符合要求的文件格式,但它們并不直接影響游戲的核心內容或開放給用戶的部分。
:“你能承認色差 (Chromatic Aberration) 是最棒的后期處理技術嗎?”
不會接受色差(chromatic aberration)作為最好的后期處理效果。在我的觀點中,色差是一種不好的視覺效果。色差和其他類似的攝像機視覺效果(如鏡頭光暈)通常不是為了增強畫面效果,而是因為鏡頭本身的缺陷,常常是不希望出現的,除非是在非常特殊的情況下。大多數時候,專業的攝像師會盡量避免出現這些效果。
色差、鏡頭光暈和漸暈(vignetting)是屬于這些“避免”的視覺效果范疇。雖然有時為了某種非常特殊的效果,可能會刻意使用這些視覺效果,但通常來說,優秀的電影攝影師在絕大多數情況下是盡量避免它們的。相反,像景深(depth of field)和虛化(bokeh)這些效果,是專業攝像師常常使用的,能夠幫助他們在每個鏡頭中實現特定的視覺效果,并增強圖像的質量。
所以,景深和虛化等后期效果是我認為值得加入到工作流程中的,它們能在圖像中產生特定的藝術效果,幫助提升整體畫面質量,而色差則完全不在這個范疇內。
“為什么不支持 DirectInput8,讓舊 USB 手柄能用,而只支持 Xbox360 手柄?”
目前,可能會支持 DirectInput 8 來兼容一些舊款的美國控制器,而不僅僅是 XInput 來支持 Xbox 控制器。雖然平臺層的工作暫時處于待完成狀態,但一旦游戲開發完成并進入發布階段,就會回到平臺層的工作上。
目前,平臺層還不支持一些功能,而這些功能可能在未來的工作中會被添加進來。因此,預計會支持 DirectInput 8,并且可能會支持其他相關的輸入控制方式。
吐槽:API 和 Dependency Walker
在討論 API 依賴時,提到有些程序在運行時需要非常復雜的依賴關系,特別是在 Windows 系統上。以前,程序的依賴關系是非常簡潔明了的,通常只需要少數幾個系統服務和庫,比如內核和用戶界面相關的調用。然而,隨著微軟不斷添加新的功能和復雜性,像是“并排程序集”和“API 資產”等概念被引入,導致了程序依賴關系的復雜化。
例如,運行一個非常簡單的程序(像是一個游戲),本來只需要幾個基礎的依賴項,但現在程序會涉及到大量的依賴,很多時候甚至無法清楚地知道程序在運行時具體依賴了什么。這讓軟件維護變得極其困難,很多依賴在程序運行時可能出現問題,導致程序崩潰或無法啟動。
有一個例子是,某些開發者在他們的電腦上運行程序時一切正常,但當嘗試在其他機器上運行時,往往會因為某些缺失的依賴或者版本不匹配而無法運行。這種情況下,有時只能通過額外安裝依賴包或者重新配置才能解決問題,而這已經不是一個簡單的復制文件就能解決的事情了。
尤其在 Windows 系統中,依賴問題變得更加復雜,很多軟件和游戲都需要安裝額外的 Redistributables(例如 VC 重新分發包)或者 DirectX 等依賴才能正常運行。這種情況使得在 Windows 上運行程序變得非常不可靠,尤其是對于商業軟件來說,安裝程序往往會捆綁大量的依賴包,但這會造成程序的運行環境變得非常復雜,甚至一旦依賴版本不一致,就可能導致程序無法運行。
為了避免這些問題,某些程序設計時非常小心,不依賴于復雜的機制,盡量保持程序的簡潔性。這樣,即使沒有安裝特定的依賴包,程序仍然可以運行。這樣的方法雖然可以避免依賴問題,但在現代操作系統中,依賴的復雜性越來越大,導致程序之間的兼容性和可維護性變得更加困難。
總結來說,這種復雜的依賴關系和不可靠的環境給軟件開發和維護帶來了很大的挑戰,特別是在 Windows 系統上,依賴的管理和版本控制變得越來越復雜,導致軟件開發變得不再像以前那樣簡單直接。
Dependency Walker 已經過時新的代替的軟件
https://github.com/lucasg/Dependencies
“感覺微軟內部沒人知道 Windows 為什么還能正常運行。”
在討論 Windows 操作系統的變化時,感到很遺憾的是,微軟曾經的很多優秀設計和技術,現在似乎被逐漸復雜化和低效的做法所壓制。盡管微軟內部依然可能有一些非常出色的開發人員,特別是負責內核的團隊,但如今操作系統的整體質量已經大不如前,很多優秀的設計思想被逐漸淘汰或者沒有得到充分的利用。
以前的 Windows NT 內核設計得非常智能、可靠,在很多方面都做得很好。然而,隨著時間的推移,微軟在操作系統上不斷增加復雜的層級和功能,導致了很多不必要的復雜性。這些復雜性使得操作系統的穩定性和性能大打折扣,尤其是用戶層面的一些低效編程和不精細的實現,直接影響了整個操作系統的質量。
雖然操作系統的內核部分仍然可能是高質量的,但如果操作系統的其他部分(如用戶界面和依賴管理等)做得不好,那么再好的內核也無濟于事。舉個例子,Windows 注冊表的問題,以及越來越復雜的依賴管理,都成為了軟件無法正常運行的原因。即使內核很好,也無法修復這些軟件層面的硬傷,最終導致操作系統變得非常不穩定,用戶體驗差。
此外,操作系統的開發需要非常細致的關注每個階段的工作,保證從內核到用戶層每個部分的質量。但微軟顯然沒有能夠做到這一點,導致了一些曾經優秀的部分被逐漸扭曲和喪失。這種局面讓人感到非常惋惜,因為如果微軟能夠保持對細節的關注,Windows 本來可以是一個非常優秀且穩定的操作系統,但現在的情況卻遠未達到那個理想狀態。
對于曾經使用過早期版本的 Windows NT(例如 NT 3.51)的用戶來說,這種變化特別令人失望,因為當時的操作系統是非常出色的,而如今看到它的衰退,不禁讓人感到更加遺憾。很多時候,人們并不在意一個操作系統從未好過,但如果一個操作系統曾經很優秀,卻因不斷的改動和添加復雜功能變得糟糕,那種失落感是更深刻的。
“現代桌面 OS 這么多依賴項,不是不可避免的嗎?”
復雜的軟件依賴和現代操作系統中額外的生產力工具,并不意味著這些問題是不可避免的。很多時候,操作系統在增加功能的同時引入了過多的復雜性,而這種復雜性往往遠超實際需要的功能增量。舉個例子,當比較 Windows NT 3.51 和現代 Windows 時,可以看到操作系統在功能上的提升遠遠沒有帶來與之匹配的復雜度。實際上,新增的復雜性幾乎是成倍增加的,而所獲得的功能提升卻相對較小。這就引發了一個問題:是否真的無法在不讓操作系統變得更加復雜的情況下,達到同樣的功能?
在實際情況中,這種復雜性并非不可避免。很多人認為,微軟本來完全有能力做得更好,避免將操作系統變得如此復雜。過去的 Windows NT 版本在穩定性和簡潔性上有著很高的水準,并且可以擴展新的功能,而不會導致操作系統變得難以管理。顯然,后來的一些改動并不一定是為了提升功能,而更多的是由于缺乏足夠的組織和管理,導致了很多復雜性的引入。
問題的根源很可能在于微軟的開發團隊文化。微軟內部并沒有建立起足夠嚴格的控制機制,確保每一個新的功能和 API 都能經過嚴格的審核,只有在真正有必要的情況下才會被納入操作系統核心功能中。相反,很多團隊由于政治上的資本或者內部的松懈,能夠將自己的功能隨意發布,這導致了大量的冗余和不必要的復雜性不斷被引入操作系統。像 ActiveX 和 DirectShow 這樣的技術,實際上并沒有為操作系統帶來顯著的好處,但卻增加了極大的復雜性和維護成本。
如果微軟當時能夠更好地控制開發過程,只有那些具備足夠經驗和技能的團隊才有機會對操作系統核心進行修改,那么今天的操作系統可能會更加簡潔高效,不會面臨如此多的問題。控制系統變更的文化至關重要。如果開發團隊能夠對每一個新功能的引入保持極高的標準,避免無關緊要的功能過度膨脹,那么操作系統將能夠保持更好的穩定性和可靠性,而不會被不必要的復雜性拖慢。