自由学习记录(205)

一、Unreal Engine 开发笔记

1.1 动画与 Rigging

3D 動畫 / rig 裡,COG control 常指控制角色整體重心的大控制器,通常接近 pelvis / hips / root 一帶。

1.2 Unreal Python API

Unreal Python API Documentation ------ Unreal Python 5.8 (Experimental) documentation

這個的作用遠比想像的要直接,Editor 內也存在 IKRetargetBatchOperation.h

Unreal Editor Python └ 可以 import unreal

UnrealEditor-Cmd.exe ... -run=pythonscript -script=Saved/CodexScratch/retarget_step25.py

所以你可以遷移這個腳本,然後用類似指令讓 UE 跑它。

但不是把目錄遷過去後,ProgrammaticToolset 就會自動識別或呼叫。

1.3 Blueprint 开发

Blueprint Function Library

Blueprint Function Library 的定義就是:一組 static functions,用來提供不綁定到特定 gameplay object 的 utility 功能;這類庫繼承自 UBlueprintFunctionLibrary,函數通常通過 UFUNCTION 暴露給藍圖。Epic Games Developers

你可以這樣判斷一個節點是不是這種 "全局工具節點":

節點 tooltip / Details 裡顯示:

  • Target is Kismet System Library
  • Target is Kismet Math Library
  • Target is Gameplay Statics
  • Target is Kismet String Library

這類基本就是靜態工具庫節點。

UKismetSystemLibrary::PrintString

Blueprint 编译与 K2 节点

普通 Blueprint / Data Asset └ 運行在 UE 提供的規則裡面 └ 不能改變 Blueprint 編輯器節點系統 └ 不能改變編譯器行為 └ 不能添加新的 K2 節點類型

代碼插件 Editor 模塊 └ 可以依賴 Editor / BlueprintGraph / KismetCompiler 等模塊 └ 可以註冊新的菜單項 / 節點 / 工廠 / 編譯擴展 └ 可以把某種資產包裝成一種藍圖節點體驗

代碼插件 Runtime 模塊 └ 可以提供真正的執行時類型和系統 └ 可以在遊戲執行時處理輸入 / 委託 / 組件 / Subsystem

能作為紅色節點出現,必須同時滿足四個條件:

  1. 有資產類型 └ UInputAction └ IA_Move 是一個數據資產
  2. 有編輯器節點支持 └ Blueprint 右鍵菜單知道:"看到 UInputAction 資產,可以生成一個 Input Action Event 節點"
  3. Blueprint 編譯器要能 compile 生成:IA_Move + TriggerEvent + 目標函數入口 這種綁定記錄
  4. 執行時有對應的 InputComponent 來接收並觸發

和普通 DA_WeaponConfig 完全不同:DA_WeaponConfig 只是 Data Asset,沒有專門 K2 節點,沒有編譯綁定規則,沒有執行時系統持續檢測它的狀態,所以不會自動變成紅色 Event。

"像 Event" 也要小心。它不是改了 Event 的哲學定義,而是復用了 Blueprint 的事件入口表現。復用了一套相同的表現手段,也就是在介面的節點顯示這一套,但是底層卻是完全的遠程使用。

Blueprint Header View(UE5 新功能)

如果你說的是類似 C++ 類結構:Class ├─ Components ├─ Variables ├─ Functions ├─ Interfaces,這是 Blueprint Header View。它會把 Blueprint 自動整理成類似 .h 文件的結構。

增强输入与 K2 节点扩展点

如果你指截圖裡說的 Enhanced Input 能把 Input Action 節點塞進 Blueprint 右鍵菜單、編譯成輸入綁定、執行時接到輸入棧:有,而且源碼裡是明確接口,不是口頭說法。

1. Blueprint 右鍵菜單擴展點:引擎給所有 UK2Node 留了菜單註冊虛函數。Blueprint action 數據庫會遍歷所有 UK2Node 派生類,調用每個節點 CDO 的 GetMenuActions。Enhanced Input 的節點就是接這裡:UK2Node_EnhancedInputAction 掃描 UInputAction 資產,為每個 UInputAction 創建 UBlueprintNodeSpawner 並註冊到 ActionRegistrar。

2. Blueprint 編譯擴展點:UK2Node 還給節點留了編譯展開點 ExpandNode。Enhanced Input 節點 override 它,生成中間 UK2Node_EnhancedInputActionEvent,把原始 exec pin 遷移到中間節點。動態綁定也有明確擴展接口 UDynamicBlueprintBinding。

3. 執行時輸入系統擴展點:引擎本來允許替換默認輸入類,通過 UInputSettings::DefaultPlayerInputClass 和 UInputSettings::DefaultInputComponentClass。PlayerController/Pawn 創建輸入物件時會讀這些類。

如果你指當前打開的 CoreGlobals.h / GIsRequestingExit 這種引擎退出標誌位置:沒有給插件留業務擴展點。那只是全局退出狀態和 IsEngineExitRequested() 包裝。

UBlueprintGeneratedClass 的复用机制

同一個 UBlueprintGeneratedClass 殼 └─ 舊內容清空 └─ 新內容重建。為什麼要這樣?因為 UE 裡很多地方已經拿著這個 Class 指針。如果每次編譯都換一個全新的 Class 物件,那麼所有舊引用都要被修一遍。官方文檔裡這句 "so that pointers to the class do not have to be fixed up" 的意思就是:為了避免到處修 Class 指針,所以盡量復用同一個 UBlueprintGeneratedClass 物件。

UCLASS 維護一個 Class Default Object,它本質上是這個 class 的默認模板物件。transient package 的意義是:"這個 UObject 需要存在一段時間,但它不是正式內容資產,不應該被保存。"

1.4 移动系统与物理

Velocity 与 ActorRotation

為什麼無效時要清零?因為 ABP 有時候可能暫時拿不到 Pawn,比如預覽、初始化、切換實例時。如果不清零,變數會保留上一幀舊值。

源碼裡 APawn::GetVelocity() 大概就是這個邏輯:

cpp 复制代码
if RootComponent is simulating physics:
    return RootComponent velocity
else if Pawn has MovementComponent:
    return MovementComponent->Velocity
else:
    return Zero

WASD / 輸入 → 影響 Acceleration / Velocity

Capsule / Actor 朝向 → 是另一個被控制的結果 → 可能由 CMC 根據移動方向轉 → 也可能由 Controller / Camera 轉 → 也可能被你自己的邏輯禁止、插值、鎖住、強制設定

Velocity = 當前實際怎麼移動

GetActorRotation = 當前角色根 / capsule 實際朝哪

空中:你可以把 RotationRate 變小,或禁用某種旋轉,或鎖住 yaw。結果:Velocity 還在變,ActorRotation 可能不怎麼變。

角色面朝北,角色往北跑,那很好理解:角色是在往前跑。但如果角色面朝東,角色還是往北移動:世界視角會說 "它在往北移動",但角色自己視角會說 "我在往左移動"。然後 Unrotate Vector 做的是:V_local = R⁻¹ * V_world

active transformation / passive transformation

數學、物理、機器人、圖形學裡是經典坑,正式名字就是 active transformation / passive transformation 的混淆:同一個旋轉矩陣,既可以被理解成 "把物體 / 向量真的轉過去",也可以被理解成 "物體不動,只換坐標系來描述它"。這兩種解釋經常只差一個 inverse,所以非常容易讓人感覺 "文字在偷換概念"。Wikipedia

當前 CMC 設定讓兩者高度同步,所以看起來像 Velocity 被當成了面朝方向。bOrientRotationToMovement = true,RotationRate.Yaw = -1。LocalVelocityX 大多是正數,LocalVelocityY 大多接近 0。

用的是 GetActorRotation,所以 LocalVelocity 的 X/Y 是按 Actor / Capsule 本地坐標算的,不是按預覽窗口裡 Mesh 的視覺朝向算的。

动画蓝图与 Motion Matching

原 ABP 的使用方式更像:Motion Matching 節點 → 查 PoseSearch Database → Database 裡面引用 Walk/Run/Start/Stop/Pivot/Turn 等大量動畫 → 執行時根據當前數據選片段。

後面更接近真實系統的替代方式會是:

  1. BlendSpace 2D 用 LocalVelocityX/Y 連續混合方向動畫

  2. Trajectory 看未來移動路徑,不只看當前速度

  3. Pose Search / Motion Matching 用當前姿勢 + 速度 + 軌跡去資料庫裡選最合適片段

Pawn 与 MovementComponent

位移輸入的數據最初是存儲在 APawn 類中的。當你調用 AddMovementInput 時,引擎並不會立即移動物體,而是將這個 "移動意圖" 累加到一個向量中。

  • 關鍵成員變數:ControlInputVector,類型:FVector。這是一個累加器。在同一幀內,如果你多次調用該節點(例如同時按下前和右鍵),這些方向向量會經過縮放後全部相加並存儲在這個變數中。
  • 輔助成員變數:LastControlInputVector,存儲上一幀被消耗掉的輸入向量,常用於需要知道 "上一幀玩家按了什麼方向"。

雖然數據存在 APawn 裡,但真正 "讀取" 並執行物理移動的是 Movement Component(移動組件)。

  • 消費 (Consume):在每一幀的 TickComponent 過程中,移動組件會調用 APawn::Internal_ConsumeMovementInputVector()。這個操作會執行兩件事:返回當前的 ControlInputVector 給移動組件進行物理計算,立即清空 APawn 裡的 ControlInputVector。
AddMovementInput 与 AddInputVector

Add Input Vector 更像內部發動機口;Add Movement Input 更像駕駛員踩油門的接口。日常玩家移動用 Add Movement Input,寫 MovementComponent / 特殊移動系統可能用 Add Input Vector。看不懂時的默認判斷:Target is Pawn → 通常更適合角色藍圖;Target is Component → 更底層,先不要選。

Add Input Vector └ 數據層:直接往 MovementComponent 的輸入累積器裡塞一個向量 └ Target:PawnMovementComponent / CharacterMovementComponent └ 參數合併:World Vector = 方向 * 強度。有人確實需要直接操作 MovementComponent。比如自定義 Pawn、自定義 MovementComponent、AI 移動、特殊載具、漂浮物、插件系統、非標準 Character 移動。對這些場景來說,直接給 MovementComponent 塞輸入向量是合理的。

相机与移动的关系

相機不直接轉身體了,但相機仍然決定 W 是 "朝鏡頭前方走"。現在鏈路大概是:Camera / Controller Rotation → 參與 WASD 輸入方向 → AddMovementInput / Movement → Velocity 改變。而我們之前關掉的是另一條:Camera / Controller Rotation → 直接讓 Capsule / Actor 朝向跟著轉。

如果完全去掉相機影響,WASD 應該改成固定世界方向或固定角色方向,例如:W → World X+,S → World X-,A → World Y-,D → World Y+。而不是:W → Camera Forward,A/D → Camera Right。

ControllerRotation = 玩家想看的方向,CameraRotation = 最終鏡頭組件實際擺出來的方向。

1.5 Enhanced Input 输入系统

IA / IMC / 绑定流程

IA_Move = 輸入動作定義 = "我要一個 Move 動作,它輸出 Vector2D"。IMC_Sandbox = 輸入映射表 = W/S/A/D、手柄搖桿這些鍵怎麼映射到 IA_Move。AglinaCharacter_CMC 藍圖裡的 EnhancedInputAction IA_Move 事件 = 收到 IA_Move 的 ActionValue 後,怎麼解釋這個值。

Input Modifiers 是在輸入進入 Trigger 之前修改 raw input value 的預處理器;WASD 這類 1D 按鍵可以通過 Negate 和 Swizzle Input Axis Values 被解釋成 2D 的 IA_Move。官方對 Input Modifier 的定位就是:它是 raw input value 進入 Input Trigger 之前的預處理器,例如 dead zone、換軸、取反、把軸向輸入轉換到某個空間。也就是說它本來就被設計成 "輸入值清洗 / 重編碼層",不是角色邏輯層。

為什麼不讓 IA 資產自己保存和計算當前值?因為 IA 資產是共享定義。如果兩個本地玩家、兩個 controller、兩個 possessed pawn 都用同一個 IA_Move,資產裡不能有一個全局 CurrentValue。所以正確位置只能是:共享資產(IA_Move / IMC_Sandbox)定義 "怎麼算",每個玩家執行時物件(EnhancedPlayerInput)按這個玩家的設備輸入實際計算。

真實硬件可能是 X = 0.04,Y = -0.02。鍵盤一般不太需要 Dead Zone,但搖桿、扳機、模擬觸控、某些鼠標軸就很需要,因為它們是連續值。

输入链路

鍵盤 W 被按下 → Enhanced Input 讀取當前 Mapping Context → IMC 把 W 映射成 IA_Move 的 ActionValue → Input Action 狀態變成 Triggered / Ongoing 等 → InputComponent 上的 IA_Move listener 被調用 → 進入 EnhancedInputAction IA_Move 紅色節點。

Local Player → Enhanced Input Local Player Subsystem → 持有 Mapping Context → IMC_Sandbox → W/S/A/D → IA_Move。PlayerController → PlayerInput / EnhancedPlayerInput → 處理硬件輸入 → 計算 IA_Move 當前 ActionValue / TriggerState。Pawn / Character → InputComponent → 執行時綁定 IA_Move 事件 → 調藍圖裡 IA_Move 紅色節點對應的生成函數。

執行時 Possess 後,PlayerController 控制 Character,Character 建立 / 使用 InputComponent,Blueprint 編譯產生的 IA_Move binding 被裝進去,Enhanced Input 系統觸發它。

IA_Move 紅色節點能不能觸發,不只看藍圖裡有沒有這個節點,還要看 Mapping Context 有沒有加到 Local Player。

蓝图中 IA_Move 红色节点的本质

AglinaCharacter_CMC 藍圖裡看到的 IA_Move 紅色節點,不是因為你藍圖裡顯式擺了一個 EnhancedInputComponent。而是因為:Blueprint 編譯器把這個 IA_Move 節點記錄成輸入綁定,執行時 Pawn / Controller 有一個 InputComponent,如果項目使用 Enhanced Input,它通常是 EnhancedInputComponent 類型,綁定 IA_Move → 藍圖生成函數。

不是 CreateDefaultSubobject 手動掛到 Components 面板,而是在 SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) 裡,把傳進來的 PlayerInputComponent cast 成 UEnhancedInputComponent,然後 BindAction(...)。

你看到的藍圖紅色節點:編輯器層是 K2 輸入事件節點。編譯後變成 "IA_Move + TriggerEvent + 藍圖函數名" 的綁定記錄。執行時 Pawn / Controller 擁有 InputComponent,這個 InputComponent 負責接收綁定,Enhanced Input 觸發 IA_Move 時調用綁定函數。

EnhancedInputEditor 模塊看到 UInputAction 資產,允許在 Blueprint 右鍵菜單裡生成一個特殊 K2 節點。Blueprint 編譯器把這個紅色節點編譯成一個 "函數入口 + 輸入綁定記錄"。執行時 EnhancedInputComponent 根據這個綁定記錄監聽 IA_Move,當 IA_Move 觸發,調用藍圖裡那個生成出來的入口函數。

输入架构分层

UE 平台輸入層把 OS 輸入轉成 UE 的 Key 狀態。Enhanced Input 的做法是:Keyboard / Gamepad / Mouse → Input Mapping Context → Key → Input Action → Modifiers(Dead Zone、Negate、Swizzle Axis、Scalar / Smooth / 自定義 Modifier)。PlayerController / Character → Bind Action → 收到已經整理好的 IA_Move / IA_Look / IA_Jump → 再決定 AddMovementInput / Ability / UI 操作。

進入載具換 Pawn,觀戰換 Spectator Pawn,但 PlayerController 仍然代表同一個玩家,仍然持有這個玩家的輸入、視角、LocalPlayer、網絡控制關係。

Gameplay Framework 文檔裡把 HUD 歸到玩家相關框架物件:每個 human-controlled player 有自己的 HUD 實例,並畫到自己的 viewport 上。Event ReceiveDrawHUD 的真實結構是:PlayerController / Viewport / Canvas 繪製流程 → 找到當前玩家的 AHUD 實例 → 調 AHUD::DrawHUD → 調 Blueprint 暴露入口 ReceiveDrawHUD → 進入藍圖紅色 Event ReceiveDrawHUD。

1.6 Tick 系统与执行顺序

世界 / Actor 生命周期入口

World / Actor 生命週期系統:打進 BeginPlay / EndPlay / Destroyed,依據:Actor 被放進 World、開始 Play、離開 World、被 Destroy

Tick 系統:打進 Tick,依據:Actor / Component 註冊了 Tick,並且 Tick enabled

Collision / Physics / Movement 系統:打進 Hit / BeginOverlap / EndOverlap,依據:碰撞檢測、Overlap 條件、Generate Overlap Events、Hit Events

Damage 系統:打進 AnyDamage / PointDamage / RadialDamage,依據:ApplyDamage / TakeDamage 鏈路

Input 系統:打進 EnhancedInputAction IA_Move / IA_Jump,依據:Input Action 狀態變化,InputComponent 上有綁定

HUD / UI 繪製流程:打進 ReceiveDrawHUD,依據:當前 HUD 繪製階段

Delegate / Event Dispatcher 系統:打進被 Bind 的 Custom Event,依據:某個 Dispatcher 被 Call / Broadcast

Timer / Latent Action 系統:打進 Timer Delegate / Delay 後續邏輯,依據:時間到了,LatentActionManager / TimerManager 恢復執行

Sequencer:打進 Sequencer Event,依據:時間軸播放到 Event Track 的觸發點

Network / RPC 系統:打進 Server / Client / Multicast Custom Event,依據:網路包到達、Actor NetGUID 找到目標物件、RPC 反序列化成功

Interface 調用方:打進 Interface Event / Message,依據:某個物件對目標物件發送 Interface Message

Tick Group 与 Tick Dependency

同一個 Tick Group 內,默認不保證你想要的 Actor / Component 先後順序,所以需要 Tick Prerequisite。A Actor Tick 計算角色位置 / 相機目標 / 某個數據,B Actor Tick 讀取 A 剛剛算好的數據。沒有 prerequisite,B 可能先 Tick。

tick dependency 比把一堆 Actor 移到新 tick group 更細,因為同組內很多 Actor 仍然可以並行更新;你只讓真正有依賴的物件等待。

如果順序錯了,Weapon Trace Physics 槍線會打一幀以前的位置。

Epic 官方文檔舉的例子:Inventory(背包)、Minimap(小地圖)、HUD UI 都建議放在 TG_DuringPhysics,因為允許一幀誤差。錄製(Replay)、Determinism、Rollback 則要求 Frame100 = 世界唯一狀態,而不是 Physics 是100、Animation 是99、AI 是100、Camera 是101。否則 Replay 無法完全重現,Rollback Netcode 無法準確回滾,確定性模擬(Deterministic Simulation)失效。

TickGroup、Tick Dependency 和並行 Tick 是一起設計的:只有明確了哪些數據在什麼階段可以被讀取、哪些階段已經穩定,系統才能既保持正確性,又盡可能並行執行。

官方文檔的意思其實是:只有當你發現多個 Actor / Component 存在明確的時序依賴時,才去調整 TickGroup 或 Tick Prerequisite。默認設定適用於大多數遊戲。

對於藍圖來說,你能直接接觸到的主要有:Actor 的 Tick Group(Details 面板)、Start with Tick Enabled、Tick Interval、Can Ever Tick 和 Event Tick。

真正的開發流程一般是:先正常寫邏輯 → 發現有一幀時序問題 → 先考慮 Tick Prerequisite(局部解決)→ 仍然不夠 → 再考慮改 Tick Group(整體階段改變)。例如:MovementComponent → AimComponent → WeaponComponent,只需要 Aim Add Tick Prerequisite(Movement),Weapon Add Tick Prerequisite(Aim),這樣就建立了一條局部依賴鏈。

FTickFunction 与运行时注册

為什麼一個 Component 可以在 BeginPlay 的時候,突然告訴引擎:"以後我的 Tick 要排在 CharacterMovement 後面",引擎居然就接受了?答案是:因為 Tick 順序本來就不是編譯時固定的,而是執行時註冊(Register)出來的。

每個 Tick 想成不是函數,而是一個任務(Task)。每個可 Tick 的物件內部都有一個 FTickFunction。Actor 是 PrimaryActorTick,Component 是 PrimaryComponentTick。Tick 依賴實際上是調用 FTickFunction::AddPrerequisite(...),給 TickFunction 添加前置依賴。

每個可 Tick 的 Component 內部都有一個 PrimaryComponentTick。Add Tick Prerequisite Component 實際上就是:AC_PreCMCTick.PrimaryComponentTick 調用 AddPrerequisite(CharacterMovement.PrimaryComponentTick)。註冊 Tick 依賴時,該節點不會檢查組件內部是否有自定義的 "分發器(Dispatcher)" 或委託。它操作的是組件內置的 FTickFunction 結構。執行此節點時,引擎會將 Prerequisite Component 的引用添加到 Target 組件的依賴數組中。強制執行順序:引擎的 Task Graph 在調度 Tick 任務時,會根據這些依賴關係確保執行順序。

时序组件 AC_PreCMCTick

AC_PreCMCTick 名字也印證了這一點:它不像 HealthComponent、InventoryComponent 那樣描述功能,而是直接把執行時機(Pre CMC Tick)寫在名字裡。AC_PreCMCTick 這是一個組件 / 計時器位置,它的意思是:我這個組件的 Tick 排在 CMC 前面。UpdateRotation_PreCMC 這是一個藍圖函數 / 業務邏輯,它的意思是:在 CMC 更新前,先設置這幀 CMC 要用的旋轉參數。

所以關係只有一層:AC_PreCMCTick.Tick → 調用 PreCMCTick 事件 → 調用 UpdateRotation_PreCMC。

PreCMCTick = 什麼時候執行,UpdateRotation_PreCMC = 執行什麼旋轉配置。

為什麼不直接在 CharacterMovement 後面寫邏輯?因為 CharacterMovementComponent 是引擎組件。很多項目遵循這樣的原則:Engine Component 不修改,用自己的 Component 寫自己的 Gameplay。這樣以後換 Movement Component、升級 UE、多個系統都依賴這個時機,都不用去改 CharacterMovementComponent 本身。

它是時序組件(Timing Component),不是負責 "計算",而是負責 "時間點"。CharacterMovement Tick → AC_PreCMCTick Tick → 廣播 Dispatcher(Tick)→ 其他系統收到通知。這時 AC_PreCMCTick 本身甚至可以沒有任何變數、沒有任何 Event Graph。

Make this component tick after PrerequisiteComponent 的意思是:讓當前組件在指定組件 Tick 完成之後再 Tick。

1.7 Event Dispatcher 与委托

Dispatcher 被 Call 時會觸發當前綁定列表裡的所有 events。

看到的那個左側大歸檔,在 UE 裡可以叫:My Blueprint panel → Event Dispatchers section / category。中文可以叫:My Blueprint 面板裡的 Event Dispatchers 區塊。

Event Dispatcher = 一個 "可廣播的事件槽 / multicast delegate" = 它自己不寫具體執行邏輯 = 它保存一組被綁定的事件。Custom Event = 一段具體要執行的藍圖邏輯入口 = 可以被直接 Call = 也可以被 Bind 到 Dispatcher 上。

OnBeginRotating 這個 Dispatcher → Bind Event to OnBeginRotating → 綁定某個 Custom Event。之後 Call OnBeginRotating → 觸發所有已經綁定進去的 Custom Event。

官方文檔裡也明確說,Dispatcher 菜單裡的 Event 選項會創建一個簽名正確的 Custom Event,讓它可以被綁定到該 Dispatcher;但如果這個 Event 沒有連接到 Bind 節點,那麼執行 Call Dispatcher 時它不會自動觸發。Epic Games Developers

你之前誤以為它們獨立,是因為 UI 把它們分在了兩個地方:左側 My Blueprint 的 Event Dispatchers(OnBeginRotating / OnEndRotating = 聲明 "可被別人監聽的信號")和 Event Graph 的 Custom Event(BeginRotating / StopRotating / SomeReaction = 具體執行邏輯)。但它們可以通過 Bind 連起來。

這裡最重要的一句話是:Dispatcher 不是 Custom Event。Dispatcher 是 "事件列表 / 廣播器",Custom Event 是 "可以被放進這個列表裡的執行入口"。

Reference Viewer 与循环引用

舉個最簡單的例子:BP_Player 變數 Enemy: BP_Enemy,同時 BP_Enemy 變數 Player: BP_Player。那麼 BP_Player 引用 BP_Enemy,BP_Enemy 也引用 BP_Player。Reference Viewer 就會看到 BP_Player 既是 Referencer(引用當前)又是 Dependency(被當前引用)。

Dispatcher 本身不形成反向引用

FTick Tick 這個 Dispatcher 本身不會自動形成反向引用。它只是聲明:"我有一個可以被別人綁定(Bind)的 Delegate。" 就像 int Health 並不會因為別人讀取 Health,這裡就知道是誰讀了它。

1.8 virtual 与多态

virtual 恰恰是為繼承和多態服務的。實際執行時可能指向子類函數,編譯期不完全固定。


二、C++ 与编程规范

2.1 UE C++ 编码标准

Coding Standard 是 UE 把內部工程文化外部化的一部分。用 lifetime cost 是在解釋為什麼 coding standard 是 mandatory:短期看省了 30 分鐘,長期可能讓 10 個人各浪費 2 小時。

lifetime cost 包含:initial cost(初次開發成本)、runtime cost(程序執行時成本,比如 CPU / GPU / 內存)、maintenance cost(後續維護成本),以及初次開發 + 維護 + 遷移 + 調試 + 協作 + 廢棄之前所有成本。

This field is mandatory └ 這個欄位必填。Safety training is mandatory └ 安全培訓是強制要求。expected to 預期你會這麼做,語氣比 must 軟一點,但在工程團隊裡也經常接近要求。

2.2 类型推导与 auto

cpp 复制代码
template <typename RangeType>
void PrintFirst(RangeType& Range)
{
    auto It = Range.begin();
    if (It != Range.end())
    {
        UE_LOG(LogTemp, Log, TEXT("Has first element"));
    }
}

這裡 Range.begin() 的返回類型取決於 RangeType。它可能是數組迭代器、容器迭代器、代理 iterator。你強行寫具體類型,反而破壞模板的泛型意義。返回類型來自 T::StaticClass(),不是一個固定類型。auto → decltype(...) 是為了表達 "返回類型由這個表達式決定"。符合前面那幾個 auto 使用例外,也不要寫成 "裸 auto"。寫 &、*、const,會約束 auto 的推導結果。

2.3 Lambda 表达式

\[\](const Thing& Th){ ... } 這個 lambda 就是 "條件函數"。\[\] 不是整個 lambda,\[\] 是 lambda 的 capture list 捕獲列表。一個 lambda 的基本形狀是:capture(parameters) { body }。

在你截圖裡:\[\](const Thing& Th) { return Th.GetName().Contains(TEXT("Hello")); }

捕獲方式:\[\] 不捕獲外部變數;Word 按值捕獲 Word,複製一份進去;\&Word 按引用捕獲 Word;= 默認按值捕獲用到的外部變數;\& 默認按引用捕獲用到的外部變數;this 捕獲當前物件 this。

2.4 命名空间与前缀约定

大多數 UE C++ 類型沒有被包在一個統一的大 namespace 裡。也就是 UE 不是這樣組織的:namespace Unreal { class AActor {}; struct FVector {}; }。而是大量核心類型直接在 C++ 頂層作用域:class AActor {}; struct FVector {}; class UObject {};。所以你寫的是 AActor* Actor; FVector Location;,不是 Unreal::AActor* Actor; Unreal::FVector Location;。

這裡的 global namespace 可以理解成 "一個包住整個引擎代碼的總 namespace"。Epic 說大多數 UE code 目前沒有這麼包。它不是說 UE 完全不用 namespace;UE 裡面當然有 UE::、Algo、Chaos、Audio 這類 namespace。它說的是:歷史上大量核心類型直接在全局作用域,而不是被包在一個總 namespace 裡。

為什麼這重要?因為如果沒有總 namespace,名字更容易撞。所以 UE 用前綴規則補償:UObject 用 U,AActor 用 A,FVector 用 F,TArray 用 T,EBlendMode 用 E。這些前綴在 UE 裡承擔了一部分 namespace 的工作:讓類型名在全局作用域裡仍然比較不容易混淆。普通輔助代碼可以用你自己的 namespace 包起來,不要期待 Unreal::AActor 或 Unreal::FVector。

2.5 编译器与平台差异

编译器分叉的历史

沒有一個乾淨的 "第一次分叉"。更準確的歷史起點是:C/C++ 從一開始就不是一個抽象雲語言,而是跟具體機器、OS、ABI 綁在一起;分叉最早來自硬件 / 系統差異,不是來自程序員故意搞生態割裂。

最早的關鍵點是 Dennis Ritchie 的早期 C compiler,強綁定 PDP-11,compiler 裡知道很多 PDP-11 的機器細節,目標是讓 Unix 在當時的機器上跑起來。後來 Stephen C. Johnson 的 Portable C Compiler / PCC 在 1970s 中期出現,目標是更容易適配不同機器架構,只把少數文件做成 machine-dependent,這樣 C / Unix 才更容易跨機器傳播。

所以第一層 "分叉" 不是 "大家都用一個 compiler 就好了,為什麼分開?",而是 PDP-11 的 compiler 不能自然滿足 VAX、Motorola、Intel、不同 Unix、後來的 Windows、不同 ABI、不同 object format。因為 compiler 最後不是輸出 "C++",它要輸出某台機器 / 某個操作系統能鏈接和執行的東西。

compiler 後端涉及:指令集、暫存器規則、函數調用約定、棧佈局、object file 格式、linker 規則、debug 信息、符號導出規則。這些東西不是 C 語法層能統一解決的。

具體衝突例子:Windows DLL export 用 __declspec(dllexport) / __declspec(dllimport),微軟這套擴展服務的是 Windows DLL / import library / linker 模型。GCC / Clang visibility 更常見的是 attribute((visibility("default")))、attribute((noinline))。它們表達的意圖可能相近,但底層不是同一個生態:MSVC / Windows 用 PE/COFF / DLL / import library / Windows ABI / Visual Studio build/debug 生態;GCC / Clang / Unix-like 用 ELF / Mach-O / shared object / dylib / visibility attribute / POSIX 工具鏈。

這就是分叉的具體衝突:同一個 "導出符號 / 不內聯 / 對齊 / 調用約定 / SIMD intrinsic" 意圖,在不同平台上需要不同的編譯器入口。

嚴格說,從 C 離開最初 PDP-11 / Unix 環境、開始 port 到不同機器時就已經出現了。PCC 的出現本身就是證據。

编译器扩展与标准

編譯器(MSVC / Clang / GCC)各自支持標準,也有自己的擴展。標準 C++ feature 是 C++20 / C++23 規定的東西,編譯器聲稱支持這個標準,就應該實現。標準庫 feature 如 std::vector、std::unique_ptr 更像 "函數 / 類庫",大部分可以用 C++ 寫。compiler extension 如 __declspec、attribute、_builtin*、#pragma、intrinsics 不是普通 C++ 庫,需要 compiler / parser / optimizer / backend 認識。

UE_NOINLINE 表達統一意圖:不要內聯。MSVC 展開成 __declspec(noinline),Clang / GCC 展開成 attribute((noinline)),不支持的平台展開為空,至少代碼還能編譯。

预处理器条件块

在編程語境裡,conditional 也可以名詞化:a conditional 一個條件語句 / 條件結構 / 條件判斷,conditionals 多個條件結構。preprocessor conditionals 意思是預處理器條件塊。完整形式本來可能是 conditional directive(條件指令)、conditional statement(條件語句)、conditional block(條件塊)。

used sparingly 的意思:允許存在,但必須像危險品一樣集中管理。

2.6 头文件与物理依赖

physical 指的不是執行時物件關係,而是源碼文件層面的關係。A.h include B.h,A 文件在物理文本上依賴 B 文件,B 一改,A 可能要重新編譯,A 被很多地方 include,影響會繼續擴散。

.h 是公開承諾,被別人 include,要輕、穩定、少牽連。.cpp 是實現細節,可以 include 更多東西,改動影響相對局部。

include what you need directly do not rely on indirect includes。.cpp 只 include 自己的 .h 看起來最乾淨,但它會把實現代碼自己的真實依賴藏起來。UE 這裡要的是:.h 管公開依賴,.cpp 管實現依賴,兩邊不是重複勞動,而是分工。.h 寫別人使用這個類時必須知道的東西,盡量輕,能 forward declare 就不要 include,因為它會被很多文件包含。.cpp 寫這個類怎麼實現,需要調用誰的函數,就在 .cpp 裡 include 誰,因為實現依賴不應該污染 .h。

2.7 默认成员初始化

.h 裡的默認成員初始化 └ 可讀性好 └ 默認值靠近屬性聲明 └ 但改默認值 = 改 header └ 依賴文件要 rebuild └ 對 engine patch 不友好。

.cpp 構造函數裡的初始化 └ 默認值離屬性聲明遠一點 └ 可能重複 └ 但改實現更容易做 patch └ 不一定牽動所有依賴 header 的代碼。

添加一個新變數 B,一般不應該讓已經手動改過的 A 丟掉。新屬性加載舊資產時會從新 CDO 拿默認值;舊屬性如果還存在、名字類型穩定,已保存的實例值應該繼續讀出來。官方也說添加 / 刪除 UProperties 時,加載已有內容通常會被處理:新屬性從新 CDO 拿默認值,刪除的屬性被忽略。

所以這段文檔的真正邊界是:Default Member Initializer 寫在 .h 讀起來清楚,但改默認值會改 header,會牽動依賴編譯和 engine patch 策略。

2.8 代码审查与 AI 可读性

代碼不是小說。小說作者寫完後,文本可以基本靜止;代碼寫完後還要面對新平台、新編譯器、新需求、新 bug、新用戶行為、新安全問題。

AI-readable code ------ 降低 AI agent 理解成本。AI 會放大 "規則是否明確" 的差異。Google 的 code review 文檔裡有一句很接近這個邏輯:技術事實和數據優先於意見和個人偏好;風格問題上,style guide 是權威。

GitHub 現在已經支持 repository custom instructions,讓 Copilot 獲得 "怎麼理解項目、怎麼 build、test、validate changes" 的上下文;還支持 repository-wide、path-specific、agent instructions。以前代碼審查裡很多東西是 "這個 reviewer 喜歡這種風格",這些東西對新人很痛苦,對 AI 也很痛苦。AI 會逼工程組織把這些隱性判斷外顯成規則。AI 讓含糊的工程文化變得更難忍受。因為人還可以靠社交關係、資歷、會議、問原作者來補上下文;AI 主要靠文本、代碼結構、工具反饋、測試結果來補上下文。你不給它穩定入口,它就會亂猜。

2.9 以用户为中心的开发理念

user-centric 以用戶為中心,用戶體驗、用戶需求優先。data-centric 以數據為中心,數據結構、數據流、數據資產是核心。artist-centric 以藝術家使用體驗為中心,工具做得更可視化、更少代碼、更適合美術調參。editor-centric 以編輯器操作為中心,重點是 UI、面板、資產、可視化流程。Blueprint-centric 以藍圖工作流為中心,重點是節點、事件圖、資產化邏輯、非 C++ 開發者。

默認讀者是會寫 C++ 的工程師。

2.10 文件命名规范

File names should not be prefixed where possible └ where possible = in cases where it is possible。where appropriate = in cases where it is appropriate;where necessary = in cases where it is necessary;where applicable = in cases where it is applicable。

JavaDoc 在這裡不要理解成 "Java 語言專用"。它指的是一種文檔註釋風格:用結構化註釋讓工具能抽取 API 說明。C++ 圈裡更常見的名字是 Doxygen,但 Epic 這裡沿用了 JavaDocs style 這個說法。


三、英语词汇与语言学

3.1 习语与短语

a cog in the machine:機器裡的一個小齒輪 → 體系裡不起眼但被運轉利用的一員。

"pad" 強調的是一個 "可站立 / 可交互的生成區域",而不是純坐標點。

narrow down our search = 縮小我們的搜尋範圍 / 排查範圍。

evangelist:原始宗教義是傳播福音的人 / 傳教者;tech industry 裡的借用義是傳播某個技術、平台、生態的人。

tight-knit:緊密連接的,人和人之間聯繫密切,常用於 community / team / group。

Epic has a presence:Epic 在某個地區有存在感 / 有業務存在 / 有人、有活動、有開發者關係網絡。

groaning:groan 的動名詞,低聲呻吟 / 發出 "唉------""呃------" 這種聲音。這裡不是受傷呻吟,而是觀眾突然意識到自己以前白費力氣後的集體懊惱聲。

propagate /ˈprɑːpəˌɡeɪt/ through a system. -pag- (spread, extend)。Think: push forward through a chain。UE: transform propagation;Networking: route propagation;Physics: wave propagation;Biology: cell propagation。

invariant /ɪnˈveriənt/:something that does not change under a particular transformation。"The speed of light is invariant in special relativity." "An object's ID remains invariant during this operation." asset GUID should remain invariant even if references change。"Invariant with respect to what?"

3.2 语法要点

software 通常不可數。你一般不說 a software,而說 a piece of software、a software program、an application、a tool、a system。

piece of + 不可數名詞 把不可數東西變成一個 "單位"。常見例子:a piece of information(一條信息)、a piece of advice(一條建議)、a piece of evidence(一項證據)、a piece of equipment(一件設備)、a piece of furniture(一件家具)、a piece of code(一段代碼 / 一塊代碼)。

3.3 词源与发音

biscuit 的词源

US biscuit 不是中文餅乾,更像鬆軟小麵包、司康、快速麵包,常配黃油、果醬、gravy。Cambridge 直接把英式 biscuit 解釋為 "US cookie",也就是英國人說 biscuit,美國人多說 cookie。Merriam-Webster 也說明它的詞源來自早期法語,意思是 "twice-cooked / baked twice"。

為什麼 cuit 這串字母在 biscuit 裡讀 kit?原因是 biscuit 不是現代英語自己按規則拼出來的詞,而是從法語來的舊拼寫。它的來源可以拆成:bis = twice / two times;cuit = cooked,來自法語 cuire / cuit。所以詞源結構是 biscuit → originally: twice-cooked,不是 "bis + kweet"。

同類型你可以抓這幾個:biscuit └ cuit = kit └ BIS-kit。circuit └ cuit 也不是 kwee-t └ 常讀 SUR-kit。conduit └ duit = dit └ CON-dit。

但這裡要小心,不要總結成 "所有 ui 都會變成 /ɪ/",英語不是這麼乾淨的系統。對比:suit └ soot /suːt/ └ 不是 sit;fruit └ froot /fruːt/ └ 不是 frit;build └ bild /bɪld/ └ ui = /ɪ/。


四、工具与自动化

4.1 AutoHotkey

.ahk 是 AutoHotkey 腳本文本文件,是 AutoHotkey 讀取執行的源碼腳本。.lnk 是 Windows 快捷方式文件。Startup 文件夾裡的 .lnk → 指向 AutoHotkey64.exe → 參數帶上你的 .ahk 腳本路徑 → Windows 登錄後自動啟動這個腳本。所以它不是編譯後的程序,而是 "快捷方式啟動解釋器,再讓解釋器運行腳本"。

你問 "找不到編譯的東西為什麼還可以有效":因為編譯不是必須的。AutoHotkey 有兩種運行方式:方式 1 解釋運行 AutoHotkey64.exe xxx.ahk,這就是現在用的方式,已經足夠後台常駐和開機自啟。方式 2 編譯成 exe Ahk2Exe.exe xxx.ahk → xxx.exe,這個只是把腳本打包成獨立 exe,方便沒有安裝 AutoHotkey 的機器運行。

截圖裡的右鍵菜單不是寫的,是 AutoHotkey 默認托盤菜單。它說明這個腳本現在像一個後台小軟體一樣在運行。這些菜單項大概是:Reload Script 重新加載腳本、Edit Script 編輯當前 .ahk 文件、Suspend Hotkeys 暫停熱鍵、Pause Script 暫停腳本線程、Exit 退出腳本、Window Spy AHK 自帶窗口檢測工具、Help 幫助。

4.2 IDE 工具

Workspace Whiz └ 老牌 Visual Studio 插件 / 工具 └ 用於快速跳文件、跳符號。Visual Assist └ Visual Studio 的 C++ 增強插件 └ UE C++ 用戶以前 / 現在都常見 └ Open File in Solution 就是快速打開 solution 裡的文件。

一條功能鏈比如 Enhanced Input:資產 UInputAction → 右鍵菜單 UK2Node → 編譯展開 → 動態綁定 → UEnhancedInputComponent → UEnhancedPlayerInput。這比手動連十遍藍圖更能提升判斷力。讀一個編輯器行為比如 "為什麼右鍵菜單會出現 IA_Move":從 BlueprintActionDatabase 到 UK2Node::GetMenuActions。這會讓你知道 "普通 BP 不能改節點系統,插件 C++ 模塊可以"。讀一個執行時主循環比如 Launch.cpp → FEngineLoop::Tick。預處理器把這些 header 內容按順序插進來。


五、物理与科学

5.1 GPS 与相对论

第一層是狹義相對論:衛星高速運動,運動的鐘會變慢,衛星鐘相對地面每天慢約 7 微秒。這來自 "速度越快,時間走得越慢"。GPS 衛星繞地球高速運動,所以從地面參考系看,它的鐘會稍微慢一點。

第二層是廣義相對論:衛星在高空,地球引力比地面弱,引力弱的地方鐘走得更快,衛星鐘相對地面每天快約 45 微秒。這來自 "引力場越強,時間走得越慢"。地面更深地處在地球引力勢阱裡,所以地面鐘比高空衛星鐘慢一點。

兩個效應相加:廣義相對論使衛星鐘每天快約 45 μs,狹義相對論使衛星鐘每天慢約 7 μs,結果衛星鐘每天淨快約 38 μs。

Neil Ashby 關於 GPS 相對論的綜述指出,GPS 依賴衛星和地面上的高穩定原子鐘,而這些鐘存在引力頻移和運動頻移;如果不仔細處理相對論效應,系統就無法正常工作。PMC GPS World 也給出同一個數量級:GPS 衛星鐘相對海平面靜止鐘每天大約快 38 微秒。GPS World

**為什麼 38 微秒會變成十公里?**因為 GPS 用的是光速傳播的無線電信號。光一秒走大約 300,000 公里,所以:38 微秒 = 38 × 10⁻⁶ 秒,距離誤差 ≈ 300,000 km/s × 38 × 10⁻⁶ s ≈ 11.4 km。所以 "每天偏十公里" 不是誇張修辭,而是從時間誤差直接乘光速得來的數量級。

GPS 把 "時間測量" 放大成了 "空間定位"。微秒級時間差,在日常生活裡幾乎不可感知;但乘上光速之後,就立刻變成公里級距離差。

原子鐘再準,也不是脫離環境的 "絕對時間機器"。它只是用原子躍遷頻率當節拍器。問題是:節拍器本身處在不同速度、不同引力勢的位置時,物理時間流速就不一樣。這不是機械誤差,不是質量差,不是 "這塊鐘不準",而是同一種鐘放在兩個物理環境裡,本來就會按不同速率累積時間。

GPS 衛星的鐘在發射前就會預先調低頻率,讓它到了軌道上以後,受相對論影響後,剛好接近地面 GPS 時間。Penn State 的 GNSS 教材給出的例子是:衛星鐘不是設成標準 10.23 MHz,而是預設為 10.22999999543 MHz;上天後因為相對論淨變快,才對齊 GPS 系統時間。EMS Online Courses

你覺得 "微乎其微",是因為你還在用日常時間尺度理解它。但 GPS 的尺子不是米尺,是光速。1 納秒誤差 → 光走約 30 厘米;10 納秒誤差 → 約 3 米;1 微秒誤差 → 約 300 米;38 微秒誤差 → 約 11.4 公里。所以 38 微秒在 "人類感覺" 裡很小,在 "光速測距" 裡非常大。

真正的偉大不在 "每天修正幾十微秒",而在這件事說明:人類日常直覺裡時間像背景,大家共享同一個 now;GPS 工程現實裡時間是一個必須建模的物理變數,速度會改它,引力會改它,改一點點,乘光速後就是公里級。

5.2 宇宙观测

因為宇宙不是空氣。地球上的 fog 是大量水滴 / 塵埃顆粒密集懸浮,光走幾米到幾百米就被反覆散射;星系際空間的粒子密度低到離譜,絕大多數方向上光可以走幾百萬、幾千萬甚至幾十億光年才被顯著吸收。宇宙當然有 "霧":塵埃雲、星際氣體、星系盤、等離子體、宇宙微波背景、引力透鏡等都會影響觀測,但不是每條視線都被擋死。

更重要的是波長。NASA 解釋過,塵埃顆粒對可見光吸收 / 散射很強,因為顆粒大小和可見光波長比較合適,所以可見光會被擋住;這也是為什麼銀河系中心在可見光下看不清。NASA Science 但 EHT 用的是 1.3 mm 波長,比可見光長幾千倍。很多塵埃對這種毫米波不再像對可見光那樣強烈散射,所以它更能 "穿霧"。

"這麼遠還能看見" 還有一個原因:M87* 周圍不是一個暗小球,而是一個極強射電源。黑洞本身不發光,但附近高溫等離子體、吸積流、噴流會發射強射電輻射。M87 本來就是著名的強活動星系,噴流可以在很多波段被觀測到。EHT 看的是這個強射電源在黑洞尺度上的結構,不是看黑洞表面。

5.3 相对论中的其他概念

相對論裡 "剛體不可能"。有些 "幾何點" 可以看起來超光速,比如激光點掃過遠處牆面,或者影子移動得很快,但那不是同一個物體在移動。


六、认知与学习

6.1 "停顿一秒" 的神经科学视角

"停頓一秒" 可能不是為了多想一下,而是讓刺激停留在某種尚未被上層語義、習慣反應、舊路徑接管的時間窗口裡。

重點不是 "英文 vs 中文",也不是 "理解 vs 翻譯"。這兩個只是表層名字。更底層的問題是:一個連續刺激進入神經系統以後,在大約一秒這個時間厚度裡,還沒有完全坍縮成既有反應;它仍然處在可重新綁定、可重新取樣、可重新定相的狀態。

6.2 SDT 自我决定理论

人的高質量動機依賴 autonomy、competence、relatedness,也就是自主感、勝任感、連接感。它特別強調,控制性環境會削弱人的 volition and initiative。

6.3 学习方法论

內部怎麼做,我之後再看 ------ 讀者視角。

在 Epic 語境裡 CIS 可能是內部 Continuous Integration System / Code Integration System 一類名字。


七、其他零散笔记

buoyancy

GIsFirstInstance = true(允許使用 deprecated 符號,不要報警)

帶 Static / Skeletal Mesh Component 的 Blueprints 實時渲染 thumbnail。

UE 文檔裡的寫法沒有直接寫成 concept StaticClassProvider = ...,而是寫成一個 struct CStaticClassProvider,裡面放一個 Requires 模板函數。Epic 因此把它叫做 concept-alike struct type,也就是 "長得像 struct 但行為像 concept 的類型"。

**摘要:**本文是一份跨领域的个人开发笔记合集,涵盖七大主题:Unreal Engine 底层机制(动画、Python API、Blueprint 编译与 K2 节点、移动系统与物理、Enhanced Input 输入系统、Tick 系统与执行顺序、Event Dispatcher 与委托、virtual 与多态)、C++ 与编程规范(编码标准、类型推导、Lambda 表达式、命名空间、编译器差异、头文件物理依赖、代码审查与 AI 可读性等)、英语词汇与语言学、工具与自动化(AutoHotkey、IDE 工具)、物理与科学(GPS 与相对论、宇宙观测)、认知与学习(神经科学视角、自我决定理论、学习方法论),以及其他零散笔记。笔记以问题驱动的方式组织,适合在遇到具体技术问题时快速查阅对应上下文。

相关推荐
峥嵘life3 小时前
Android GMS 开机向导横竖屏切换配置总结
android·学习
六bring个六3 小时前
链表学习(常规链表)
数据结构·学习·链表
say_fall3 小时前
【Git 精品详解】企业规范:企业级开发模型、Git Flow、发版流程规范、Code Owner 制度、事故应急回滚
大数据·linux·服务器·git·学习·elasticsearch
kgduu3 小时前
react-redux学习笔记
笔记·学习·react.js
诗句藏于尽头4 小时前
Ai-3D音乐播放器-Mineradio -VibeCoding大赏-新增歌词字体自定义上传功能(个人学习)
学习
TE-茶叶蛋4 小时前
Node.js-Phase 1 学习总结:CLI 文件管理系统
学习·node.js
Waay13 小时前
面试口述版:个人对 Prometheus 完整理解
运维·学习·云原生·面试·职场和发展·kubernetes·prometheus
一楼的猫16 小时前
AI写作合规技术方案:平台检测机制分析与规避策略
人工智能·学习·机器学习·ai写作
四月天4317 小时前
web安全-SSTI(服务器模板注入)
笔记·学习·web安全·网络安全