人类玩家
AutoPossessPlayer
AI 控制器
AutoPossessAI
也就是:
-
这个 Pawn 开局要不要被玩家接管
-
这个 Pawn 开局要不要被 AIController 接管
class APawn : public AActor
{
public:
UPROPERTY(EditAnywhere)
TEnumAsByte<EAutoReceiveInput::Type> AutoPossessPlayer;
};
AZMDCameraPawn 继承了 APawn,自然就直接有这个属性。
AutoPossessPlayer = EAutoReceiveInput::Player0;
字段本身占 1 字节
按 1 字节对齐
在 APawn 对象内存布局中的偏移位置
误解 1:Player0 = 世界上第一个玩家
不对。
它是本机本地玩家槽位 0。
误解 2:设置了 Player0 就会自动生成 Pawn
不对。
它只解决"谁来控制这个 Pawn",不解决"这个 Pawn 是谁生成的"。
Player0 / Player1 / Player2 / Player3
本质上是 本机上的 LocalPlayer 下标,不是项目设置里某个永久数字。
所以它是运行时状态,不是主要靠 Project Settings 静态查看的。
Project Settings 里通常只能看到和本地多人相关的规则,例如:
-
是否启用分屏(Split Screen)
-
本地多人时屏幕布局怎么分
-
默认输入映射之类
这些只能说明"项目支持怎样的本地多人表现",不能说明当前这一次运行已经有几个 LocalPlayer。
最直接的查看方式:运行时看 GameInstance
在 UE 里,本地玩家通常挂在:
-
UGameInstance -
里面维护
LocalPlayers
所以最稳的判断是看:
-
GetGameInstance()->GetNumLocalPlayers() -
或者
GetGameInstance()->GetLocalPlayers().Num()


-
从
GameInstance出发 -
看 LocalPlayers 数组数量
EAutoReceiveInput::Type 的意思不是"某个对象的成员",而是:
在 EAutoReceiveInput 这个作用域里面,取出名为 Type 的东西。
所以 :: 的本质是:
"到某个名字空间/类/结构体/枚举作用域里去找一个成员类型、静态成员、枚举值、函数名等。"


TEnumAsByte<EAutoReceiveInput::Type> 又是什么意思
把 EAutoReceiveInput::Type 这个枚举类型,包装成一个按 1 字节存储的类型。
因为老式 enum 底层大小不总是稳定,
UE 为了节省内存、序列化、反射兼容等原因,常写:
TEnumAsByte<某个枚举类型>
::
作用域解析
overflow-visible!
AActor::BeginPlay
std::vector
EAutoReceiveInput::Type
意思都是:
"去某个作用域里找名字"。
overflow-visible!
AMyPawn::BeginPlay()
意思是:定义 AMyPawn 这个类的成员函数。
overflow-visible!
Super::BeginPlay();
AActor::BeginPlay();
意思是:调用父类版本。
枚举作用域::枚举值
现代 C++ 常见:
overflow-visible!
C++
EThing::ValueA
命名空间::类型
overflow-visible!
C++
std::string
UE::Math::TVector

你可以把它和文件系统类比一下,这个类比很贴:
A::B
很像:
从 A 这个目录进去,找到 B。
但 B 可以是很多东西:
-
一个文件
-
一个子文件夹
-
一个程序
-
一个配置项
所以 :: 只负责"路径限定",不负责"B 的种类定义"。
GetController() 返回的不是 APlayerController*,而是更上层的:
overflow-visible!
AController*
也就是说,Pawn 并不知道"控制我的一定是玩家控制器"。
因为在 UE 的体系里,控制 Pawn 的可能是:
-
APlayerController -
AAIController -
甚至某些你自己继承的 Controller 子类
所以 GetController() 只能返回它们共同的父类 AController*。
AController* SomeController = GetController();
overflow-visible!
PlayerController->bShowMouseCursor
而 bShowMouseCursor 是 APlayerController 才有的成员,不是所有 AController 都有。




-
子类对象 → 当父类用 :通常没问题,这叫 upcast(向上转型)
-
父类指针 → 当子类用 :这才是问题,这叫 downcast(向下转型)
overflow-visible!
APlayerController* PC = (APlayerController*)C2;
编译器可能放你过去,但运行时这对象根本不是 APlayerController。
这时候你再访问 APlayerController 特有成员,就属于按错误类型解释内存,后果就是未定义行为。


只有运行时才知道的东西。
网络包这一帧收到了什么


运行时世界本来就是你的程序要面对的现实,不是编译器能替你消化掉的垃圾。

后者属于运行环境。
"那我写代码的时候,为什么还要我自己承担这些运行时判断?"
答案很简单:
因为程序就是你写的,业务前提也是你最清楚。
编译器不知道"这个 Pawn 在你的项目里永远只给玩家用",但你知道。
这里不是编译器在甩锅。
而是:
你在把一个运行时前提正式写进代码。
它至少做了这些:
-
验证
AController这个类型存在 -
验证
GetController()这个函数声明存在且可见 -
验证返回值类型可赋给
AController* -
生成相应调用约定和返回值处理代码
-
确保后续对
C的使用不超出AController接口边界 -
可能做内联、优化、寄存器分配等后续工作
"程序在执行""语言在运行"。



运行时不是"编译器",也不是"解释器",而是程序跑起来时背后依赖的一整套支撑机制。
"程序执行器",如果你想的是"到底是谁把程序跑起来",那通常要分两层看。
第一层是操作系统加载器 / 进程启动机制。
比如你双击一个 .exe,或者命令行里执行它,真正第一步不是编译器,也不是 C++ 代码自己突然活了,而是操作系统做了很多事:


如果放到 C++ / UE 语境里理解,会更清楚。
比如你写 UE C++ 代码后点击编译,大致链路是:
-
你的
.cpp被编译器编译 -
链接器把很多
.obj和库拼成模块或 exe/dll -
运行游戏时,操作系统加载这个程序
-
C++ runtime 和 UE 自己的运行时系统初始化
-
引擎主循环开始跑
-
你的
BeginPlay()、Tick()这些逻辑才在运行中被调用
所以你看到 BeginPlay() 执行时,编译器早就退场了。
这时候主要在场的是:
-
操作系统
-
可执行文件
-
C++ 运行时
-
Unreal Engine 运行时
-
你的游戏逻辑
这能帮助你切开"编译"和"运行"两个世界。
再补一个容易混的地方:
"解释型语言没有编译器" 这句话也不准确。
现代很多解释型语言也会有编译步骤,只是编译到的不是本机机器码,而可能是:
-
字节码
-
中间表示
-
某种内部指令格式
比如 Python 常常先编成 bytecode,再由虚拟机执行。
所以"编译"和"解释"不是绝对对立,现实里常常混合出现。

但"动态"发生在哪?
发生在:
-
这次运行时
Health到底是多少 -
这个对象当前是不是已经被攻击过
-
这一帧网络同步有没有更新血量
-
这个 Actor 是不是刚刚被销毁重建
-
这个
Move()最终是不是又引发别的逻辑
"我以为我写的是规则,但实际系统运行时还有一层对象状态流动、控制权转移、生命周期变化,我没真正抓牢。"
你还没有把"运行时状态绑定"当成代码系统的一部分来接受。你还更习惯把"代码"理解成纯文本规则本身。
比如某个 Component 什么时候 attach,某个 Actor 什么时候 begin play,什么时候 pending kill。
资源有没有准备好。
贴图、骨骼、网络对象、关卡子系统是否已经就绪,这些都属于运行时世界状态。
所以"动态"并不神秘。它常常只是:
程序运行时,具体实例、具体数据、具体调用目标、具体路径在不断被确定。


文本写出来,不等于那个 vibe 被你控制住
在语言哲学中,奥斯汀(J.L. Austin)区分了"描述性话语"与"施事性话语"。
-
普通文本通常是描述性的。
-
程序文本则是强施事性的。
代码并非在"描述"一个世界,而是在"构造"一个行为过程。当编译器将 MOV 指令转化为电流开关时,它完成了一次从**语义(Semantics)向效力(Effectivity)**的跨越。这种"言即行"的特性,使程序文本成为了人类历史上唯一一种能够直接驱动物理实在的文本形式。
你真的知道你在说什么吗,你不可能知道,你知道你说的话会造成什么后果吗,你也不会知道,无论是敏感传统善良,还是无限尝试,我们控制自己的言论本就难以...
所以有些部分是没有必要的,所以这一切都是结果导向的,我用着更容易伤人的话在说话,即使我不想伤人。但这根本不能只取决于你,新的语言不可避免的会伤人,如果继续用同样的思维环境,这样只会让一切停滞。
悲,物,我缺的是?wanna talk,not to who?更加的注意言行,和谁说话?
皆非自由意志的抒发,而是对已有**规则集(Ruleset)**的激活。你所感受到的"谈话",实际上是在探测并触发对方(引擎、模型、他人)预设的逻辑路径。
卢曼的"双重偶然性"(Double Contingency)与通信协议化
尼拉斯·卢曼(Niklas Luhmann)认为,任何社交系统的建立都基于"双重偶然性":我知道你具备选择性,你也知道我具备选择性。
谈话不是交流思想,而是执行规则
对 UE 说 Cast<APawn>(Actor) 时,其逻辑力度与你在现实中对人说"请给我那支笔"是等价的------你都在调用一个预设好的功能槽(Function Slot)。
所有"谈话对象"都是符号态的大他者(The Other)
每当你感到这种共鸣时,立刻在脑中将眼前的交互对象转化the other
恐惧源于你意识到自己可能只是一个函数调用。要对抗这种平庸化,你的输出必须具备某种**"非算法性"的剩余**。这种剩余不在于语义的正确性,而在于你作为观察者对系统边界的探测。



在 C++ 里,成员函数名本身不会总是自动退化成
指针形式
对于成员函数,底层模型比普通函数复杂,因为成员函数默认绑定类类型,调用时还需要对象实例。

overflow-visible!
BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
本质是在传:
-
轴名字:
LookUp -
调用目标对象:
this -
要在这个对象上调用的成员函数:
APawn::AddControllerPitchInput
为什么这里写的是 &APawn::AddControllerPitchInput,不是 &AZMDCameraPawn::...
这点很关键。
因为你的 this 是 AZMDCameraPawn*,而 AZMDCameraPawn 继承自 APawn。
AddControllerPitchInput 这个函数是定义在 APawn 里的,所以拿父类成员函数指针完全合法。
意思就是:
"把父类里现成的这个成员函数,当作回调绑定给当前这个子类对象。"
因为子类对象本来就拥有父类部分。
如果你自己在 AZMDCameraPawn 里写了一个同签名函数,也可以写成:
overflow-visible!
C++
&AZMDCameraPawn::SomeFunc
只要签名匹配 BindAxis 要求即可。
这里的 & 不是"按引用传递"
这个非常容易混。
很多人一看到 & 就下意识想到引用:
overflow-visible!
C++
void Foo(int& x)
但你这里根本不是类型声明位置,而是表达式位置。
overflow-visible!
C++
&APawn::AddControllerPitchInput
这里不是在声明一个引用,而是在形成一个值,这个值的类型是:
overflow-visible!
C++
void (APawn::*)(float)
也就是"成员函数指针值"。
所以千万别把这里理解成"对函数使用引用传递"。不是这个意思。


第一层& 出现在"类型声明里",还是"表达式里
旧输入系统的写法,就是你在 SetupPlayerInputComponent() 里看到的这种:
-
BindAxis("MoveForward") -
BindAxis("MoveRight") -
BindAxis("Turn") -
BindAxis("LookUp") -
BindAction("Jump", IE_Pressed, ...) -
BindAction("Jump", IE_Released, ...)
这种模式下,移动逻辑是"输入名 -> 绑定函数 "的直连式结构。也就是说,MoveForward、MoveRight 这些 Axis 名字先在 DefaultInput.ini 里配置,然后代码里用 BindAxis() 把它们接到 Pawn 的成员函数。空格跳跃同理,是 Jump 这个 Action 名先在配置里映射到 SpaceBar,再在代码里 BindAction() 到 StartJump/StopJump。这是 UE 旧输入体系的典型工作方式。
新的 Enhanced Input 不是这套思路。它不再依赖你单纯在 SetupPlayerInputComponent() 里写 BindAxis/BindAction 就完事,而是通常要补齐一整套新链路,比如:
-
UEnhancedInputComponent -
UEnhancedInputLocalPlayerSubsystem -
AddMappingContext -
UInputMappingContext
也就是说,新系统的"移动逻辑"不只是"绑一个名字",而是"Input Action 资源 + Mapping Context + LocalPlayer Subsystem 注入 + EnhancedInputComponent 绑定 "这一整套机制。你的文档里已经明确指出,项目里并没有找到这些 Enhanced Input 必需接入逻辑,只找到了旧式 BindAxis/BindAction。
旧:代码直接绑输入名。
新:先建立 Input Action / Mapping Context,再由 Enhanced Input 系统分发给代码。
文档里的实际修复就是把默认输入类改回传统系统:
-
DefaultPlayerInputClass=/Script/Engine.PlayerInput -
DefaultInputComponentClass=/Script/Engine.InputComponent
旧版最大的特点是简单。你在 DefaultInput.ini 里写一个 MoveForward、Jump,然后代码里 BindAxis/BindAction 接上,结束。小项目、单键盘鼠标、规则简单时,这套很好用。你这次项目里也是因为 Pawn 代码本来就是按这套写的,所以把默认输入类切回旧系统后,链路立刻就通了。
多设备和复杂映射。
现在游戏很少只考虑键鼠。你可能同时要支持键鼠、手柄、不同区域按键布局、重绑定、上下文切换。旧系统里,这些东西会迅速变成一堆分散配置和 if/else。
Enhanced Input 则是把"输入"抽象成了 Input Action,把"一组场景下的映射规则"抽象成 Mapping Context。
Enhanced Input 则是把"输入"抽象成了 Input Action,把"一组场景下的映射规则"抽象成 Mapping Context。
于是你可以很自然地表达:
-
平时一套移动键位
-
开车时另一套
-
UI 打开时再切一套
这就是它非要上新系统的第一原因:它想把输入从"按键绑定"升级成"输入语义系统"。
旧系统当然也能"改",但通常改得很硬。你得自己写逻辑维护谁生效、谁失效、哪个模式下禁用哪个输入。
新系统把这个变成显式机制:给 Local Player 加/减 Mapping Context,就像切一层输入规则。比如角色走路时启用角色输入 context,开背包时加 UI context,驾驶载具时切载具 context。
这在复杂项目里非常重要,因为输入冲突往往不是"有没有绑定",而是"当前谁该接管输入"。
输入不再只是 0 和 1。
旧系统更接近"这个键按了""那个轴有值"。
新系统更重视"输入事件的语义"和"输入值的处理链"。例如:
-
触发条件:按下、释放、持续、长按、连点
-
值修饰:dead zone、scale、normalize、反转
-
不同设备统一成同一个 Action 值
这使得"移动""瞄准""视角""冲刺"这些不再只是低级按键数据,而是统一输入语义。对于大型项目,这能明显减少输入代码的碎片化。
旧系统偏"程序员先写死名字,再接函数"。
新系统偏"策划/TA/程序共同围绕 Input Action 和 Mapping Context 组织输入资源"。
这意味着很多输入改动不必每次都去翻 C++ 绑死。项目协作时,这种资源化管理明显更稳。Epic 推新系统,很大一部分也是为了让输入和动画、增强交互、Gameplay Ability、UI 等现代 UE 工作流更一致。
不是因为旧版完全不能用,而是因为旧版的上限太低。
换句话说:
-
小项目、教学、原型、单角色基础移动:旧版完全够用。
-
中大型项目、多人设备支持、重绑定、状态切换复杂:旧版会越来越难维护。
所以"抛弃旧版"不是说旧版突然错了,而是说 当项目复杂度上来时,继续死守旧版会让输入系统越来越像补丁堆。
你这个案例正好反过来说明了这一点:
你的 Pawn 代码本来就是旧接口 BindAxis/BindAction,但配置却切到了 Enhanced Input 默认类,而项目里又没有补上 UEnhancedInputComponent、UEnhancedInputLocalPlayerSubsystem、AddMappingContext 这些新系统接线,所以输入直接断链。
这并不代表新系统差,而是代表:新系统不能只切开关,不补结构。
所以可以把结论压成三句:
旧版的优点是简单直接。
新版的优点是可扩展、可切换、可数据化。
Epic 推新版,不是因为旧版不能移动,而是因为旧版难以优雅承载"复杂输入工程"。
Enhanced Input 不算难,但它有明显的起步门槛。
门槛不在 API 难,而在于你必须先建立一套"输入资产分层"和"AI 不可越界规则"。一旦这层没立住,它会比旧输入更容易看起来高级、实际上更乱。Epic 官方也把它定义成一套由 Input Actions、Input Mapping Contexts、Input Modifiers、Input Triggers 组成的资源化输入系统,本来就是为了应对更复杂的输入场景,而不是为了最简单项目少写两行代码。
-
Action:你要做什么语义,像 Move、Look、Jump、Interact
-
Mapping Context:当前这套键位规则在哪个场景生效
-
Modifier:输入值怎么修正
-
Trigger:什么条件下算触发
也就是说,它预制的不是你游戏的控制逻辑,而是输入系统的组织方式。这就是为什么它强,但也更需要管控。
第一,先固定输入语义表,禁止 AI 自创名字。
例如只允许:
-
IA_Move -
IA_Look -
IA_Jump -
IA_Interact -
IA_Sprint
AI 不准临时发明 IA_MoveForward、IA_RunFast、IA_TestJump2 这种名字。
因为输入系统一乱,最先乱的不是逻辑,而是命名和资产数量。
常见且够用的分层是:
-
IMC_Common:Esc、菜单、截图这类全局输入 -
IMC_Player:走路、跳跃、攻击 -
IMC_UI:背包、面板、确认、返回 -
IMC_Vehicle:上车后专用 -
IMC_Debug:测试热键
AI 只能往既有 Context 里放内容,不能擅自再裂变出一堆 IMC_Player2、IMC_PlayerCombatTemp。
这一步非常关键,因为 Enhanced Input 最容易乱的就是 Context 膨胀。
碰撞检测逻辑是这样写的:
- 先取胶囊半高 CapsuleHalfHeight
- 从 Pawn 当前位置向下打一条线 Start 到 End
- 检测通道固定是 ECC_Visibility
- 如果打中并且当前在下落或静止,就认为落地 if (bHit && VerticalVelocity <= 0.0f)
- 然后把 Pawn 的 Z 直接修正到"命中点 + 半个胶囊高" DesiredZ,并调用 SetActorLocation
所以这不是完整的角色碰撞地面系统,而是"向下射线判地 + 手动吸附到地面"。
你在 box 上会穿透、在 landscape 上不会,原因就是:
- 你的地面识别只看 ECC_Visibility
- Landscape 通常默认会阻挡
Visibility - 但场景里的 box 不一定阻挡
Visibility - 如果 box 的碰撞预设没有 block
Visibility,这条地面检测线就会直接穿过去
- 你的落地逻辑依赖"打中地面"这件事
- 一旦 line trace 没打中 box,代码就不会进入 bIsGrounded = true
- 重力会继续生效,Pawn 就继续往下掉
- 所以表现出来就是穿透 box
- Landscape 更容易正常,是因为它通常有稳定的地形碰撞和
Visibility响应
- 所以你的简化逻辑在 landscape 上刚好成立
- 但对普通 static mesh box,这种写法非常脆弱
单一结论:
- 不是 landscape 特别"防穿透",而是你当前代码只把"能被 ECC_Visibility 打中的表面"认作地面;landscape 通常满足,box 不一定满足,所以 box 会穿透。


ECC_Visibility 是 Unreal 里的一个碰撞/射线检测通道枚举值 ,属于 ECollisionChannel。它不是"让物体可见/不可见"的开关,而是专门给 trace(射线检测) 用的一个"频道名"。官方枚举里它和 ECC_Camera、ECC_Pawn 这些并列存在。
"我现在发出一条射线,这条射线走的是 Visibility 这条通道;场景里每个物体都可以设置自己对这条通道是 Block / Overlap / Ignore 哪种响应。"
"发一条属于 Visibility 通道的线,看哪些物体会对这个通道产生阻挡。"
Epic 的教程里也直接把 ECC_Visibility 用作 line trace 的通道示例。
Visibility 这个名字和编辑器里"可见性/隐藏显示"不是一回事。
它只影响碰撞查询结果,不影响你肉眼能不能看到这个模型。社区里也经常有人把这两个概念搞混。
编辑器里通常会在物体的 Collision 设置里看到:
-
Object Responses
-
Trace Responses
其中 Visibility 往往就出现在 Trace Responses 下面。
这表示"当别人用 ECC_Visibility 来 trace 我时,我是阻挡、重叠,还是忽略"。Epic Developer Community Forums+1
一个非常实用的直觉是:
-
ECC_Visibility:常用于"我能不能打到/看到/点到这里"的查询 -
ECC_Camera:常用于"摄像机碰撞"这类查询
但这不是硬性物理定律,而是引擎和项目里的常见约定。社区对这两个通道的用途有不少讨论,典型说法就是 Visibility 更偏"视线/命中检测",Camera 更偏"相机避障"。Epic Developer Community Forums+1
ECC_Visibility = 给射线检测用的"可见性查询通道",不是渲染可见性。
Gameplay Ability System,也就是 GAS。
这算是 UE 很典型的"预制大菜"。官方在 Lyra 文档里直接把它描述为一个用于快速实现和迭代复杂 gameplay mechanics 的框架。它不适合所有项目,但只要你开始涉及技能、Buff/Debuff、属性修改、冷却、输入触发能力、网络同步能力,GAS 的价值就会变得很高。它的门槛比 Enhanced Input 高不少,但它解决的是"战斗/能力系统如何不烂掉"的问题。
Enhanced Input
Gameplay Tags
Asset Manager
Common UI
StateTree
然后才看 GAS、World Partition 这些更重的体系。
原因很简单:
-
Enhanced Input管输入入口 -
Gameplay Tags管统一语义 -
Asset Manager管资源入口 -
Common UI管界面和输入交汇 -
StateTree管复杂行为流 -
GAS管能力/战斗复杂度 -
World Partition管大世界规模
你会发现,这些"预制菜"真正值钱的地方,不是帮你少写一点代码,而是帮你先把系统边界立起来。
如果某个问题属于"行业共性结构问题",优先吃预制菜。
比如输入、UI 栈、资源加载、标签语义、状态组织。
因为这些地方,UE 已经替你踩过很多坑了。
tool 不是直接"看屏幕"
它先收到 MCP 请求,再把请求交给 Unreal 里的某个 handler。
也就是:
-
Roo / MCP client
-
→ Unreal MCP server
-
→ Unreal 插件里的 HandleInspectAction()
-
→ C++ 代码去访问 UObject、Editor subsystem、Selection、Property system
-
→ 再回传 JSON
-
搜狗开启了"跨程序调起候选窗 / 智能汲取焦点 / 兼容模式输入"一类机制。
-
Windows 的 TSF/Text Services Framework 状态异常,导致搜狗在没有正常文本框时也弹预编辑串。
-
某些覆盖层、悬浮窗、游戏内嵌浏览器、远程桌面、注入类软件,让搜狗错误识别输入位置。
输入系统 。
也就是你按键、鼠标、手柄这些输入,最后怎么绑定到 Jump、MoveForward 之类的逻辑上。这里才分:
-
旧输入:
PlayerInput / InputComponent -
新输入:
Enhanced Input
第二件事是鼠标点中了场景里的谁 。
这个不是"旧输入 / 新输入"在决定,核心是 PlayerController 做点击/悬停检测时,用碰撞通道去 trace ,常见就是看 ECC_Visibility。
截图里那句"box 会穿透,取决于组件碰撞对 ECC_Visibility 的响应是不是 Block",说的是:
-
这个场景物体能不能挡住鼠标点击射线
-
它是否会被"点中"
-
是否会把后面的东西"挡住"
这和你用旧输入还是 Enhanced Input,不是一层问题。
就是把这个组件对 Visibility 这个通道的响应,设置为 Block。
也就是以后凡是"按 Visibility 通道来测你"的 trace / sweep,都会把你当成可命中的阻挡体。Visibility 属于 UE 的默认 Trace Response 通道之一,常用于可见性测试、Line Trace by Channel 这类射线检测。