Actor 这个词,我在带新人入行游戏开发时几乎每次都要解释一遍。它看起来像什么高深的底层概念,实际上你打开任何一款游戏,里面那个能动的角色、你踩的地板、头顶的光源、触发剧情的气团,全都是 Actor。说白了,Actor 就是放进游戏世界里的一切东西的统称。今天这篇是"第 3 天"系列里我非常想好好写的一篇,因为很多新手在学 Unreal Engine(UE)时,第一个被卡住的名词就是它,而一旦理解清楚 Actor 和它周围那一圈概念的关系,后面看任何教程都会顺很多。
这篇内容适合完全没接触过 UE 的人,也适合从 Unity 等引擎转过来的朋友。文章里我会从最底层的概念讲起,再带你走一遍在编辑器中创建第一个 Actor 的完整流程,最后补几个我这些年见过最多人踩的坑。只讲理论不给操作的教学没什么用,我尽量让每一步你都能自己复现出来。
1. Actor 的本质:场景里一切看得见摸得着的"东西"
1.1 从层级关系看 Actor:你的游戏世界不是一张平面图
你在 UE 编辑器里打开一个关卡,看到的是一片世界。这片世界不是一张静态图片,它是由很多独立的物件叠加组成的。这些物件,在引擎里统一叫 Actor。
举个例子,你在场景里放了一个方块,引擎里其实发生了几件事:场景里多了一个 Actor,这个 Actor 内部挂着一个方块形状的静态网格体组件(Static Mesh Component),组件里塞了一份方块模型资源,模型上还贴着一份材质。对玩家来说,他看到的是"一个方块",但对引擎来说,这个方块是一个带组件的 Actor。
这就是 UE 最核心的架构思路:Actor 是"存在"的单位,Component 是"能力"的单位。一个 Actor 默认只是个空壳,你给它加一个 Static Mesh Component,它就能被看见;你给它加一个 Physics Constraint,它就能被物理引擎接管;你给它加一段音频组件,它就能发声。Actor 本身不负责具体功能,它负责把这些组件组织起来,并让自己被引擎的生成、销毁、Tick 流程管理。
1.2 Actor 和 GameObject:不同引擎里相同的"物件哲学"
很多从 Unity 转过来的朋友第一反应是:"这不就是 GameObject 吗?"对,它们本质上是同一个东西,都是场景中所有实体的基类,但用法上有不少差异。
Unity 的 GameObject 本身的能力更弱,几乎所有行为都靠 AddComponent 挂脚本。而 UE 的 Actor 天生带了一套生命周期管理:BeginPlay、Tick、EndPlay 这些函数是 Actor 自带的,你不用额外挂脚本就拥有这些事件。另一个重要区别是 UE 里 Actor 可以由蓝图实现,也可以由 C++ 实现,甚至两者混合,而 Unity 的 GameObject 本身不是脚本,脚本需要单独挂载。
从设计哲學上说,Unity 更偏向"组合优于继承",什么都往 GameObject 上堆组件;UE 则允许你用继承来派生新的 Actor 类型。你完全可以创建一个 BP_EnemyAnimal 继承自 BP_Animal,再继承自 AnimalBase,每一层添加自己的逻辑和组件。这种方式在大型项目中非常实用,因为同类型的 Actor 可以直接共享父类里写好的逻辑。
1.3 Actor 的类型:为什么会有这么多 Actor 子类
UE 里你几乎不会直接用一个裸的 Actor,而是用它的子类,因为每个子类针对不同场景预装好了能力。搞清楚几个最常用的子类,你读文档、看教程的时候会轻松很多:
| Actor 类型 | 主要用途 | 关键特性 |
|---|---|---|
| AActor | 最基类,通用物件 | 自带 Transform、生命周期,啥都可以做 |
| APawn | 可以被控制器控制 | 适合做角色、载具、可以接收输入 |
| ACharacter | 带骨骼和移动能力的 Pawn | 内置 CharacterMovementComponent,适合做人类角色 |
| APlayerController | 接收玩家输入 | 负责控制 Pawn,处理 HUD 和输入 |
| AStaticMeshActor | 场景里的静态物体 | 只挂了一个 StaticMeshComponent,用来放方块、椅子、建筑 |
| ACameraActor | 摄像机 | 用于拍摄视角 |
| AActor 的各种蓝图子类 | 你自己定义的东西 | 比如 BP_Door,BP_Enemy,BP_Item |
我第一次学的时候以为必须记住所有这些类的继承关系才能动手,其实根本不需要。你只需要记住:绝大部分时候,你从"蓝图类"里创建,选择 Actor 作为父类,然后往里面加组件就行。后面用到 Pawn 和 Character 时再回头区分它们的差别,完全来得及。
2. 创建第一个 Actor 前的准备:理解坐标与场景结构
2.1 为什么 Unity 转过来的新手容易在坐标上翻车
创建 Actor 之前,你需要先搞懂 UE 的坐标体系。UE 是左手坐标系,X 轴朝前,Y 轴朝右,Z 轴朝上。Unity 也是左手坐标系,但有一个非常容易让人混淆的点:Unity 的 Forward 在旋转为 0 时指向 Z 正方向,而 UE 的 Forward 是 X 正方向。
这个差异会导致一个很经典的翻车现场:你在 Unity 里习惯把一个物体放在 (0, 1, 0) 让它往前移动一段,在 UE 里你想让物体沿自身前方移动,写的是GetActorForwardVector()乘以速度,而这个 ForwardVector 在世界坐标里是 X 方向。如果你不知道这个区别,写移动逻辑时候很容易出现"我想让它往前走,结果它往侧面飞了"的情况。
坐标的另一个要点是 UE 的世界中心不是 (0,0,0) 本身,而是你关卡里的原点。每次新建一个关卡,原点都在地图中央地面上(缩略图里那个小红圈的位置)。你创建的第一个 Actor 如果不设置坐标,它会出生在 (0,0,0),也就是世界里那个原点上。
2.2 视野里的 Level、World、Actor 三者关系
创建 Actor 前还要把"关卡(Level)"和"世界(World)"这两个词的关系理清楚。World 是整个游戏运行时的最高容器,一个世界可以有多个关卡,常见的是持久关卡(Persistent Level)加上若干子关卡(Sub-Level)。你平时编辑的那个场景,其实是 World 中的一个 Level 里的内容。
Actor 必须存在于某个关卡里,不存在"游离在世界之外"的 Actor。这带来一个非常重要的实操认知:当你要在运行时动态生成一个 Actor 时,必须告诉引擎"生成到哪个关卡"。大部分时候你直接用GetWorld()拿到当前世界,引擎会自动帮你决定放到当前关卡,但你自己写代码时要注意,如果你在某个子关卡里调用生成,而当前可见关卡不是它,可能会出现物体在但你看不到的情况。
知道这三个词的关系之后,创建 Actor 前的基础知识就齐了。我强烈建议你不要直接跳到编辑器里乱点,先在心里建立这个模型:你在编辑器里拖进场景的每一个东西,都会被引擎打包成一个 Actor 的实例,存进当前 Level 的文件里,游戏运行时由 World 统一管理他们。
3. 手把手创建第一个 Actor:从空 Actor 到看得见的方块
3.1 创建 Blueprint Actor 的完整流程
现在进入实际操作。UE 项目里创建 Actor 有两种主流方式:蓝图和 C++。不管你是哪种开发者,我都建议从蓝图开始走一遍流程,因为蓝图能让你直观看到 Actor 的组成结构,不会被编译流程干扰。
具体步骤如下:
- 打开你的项目,在 Content Browser(内容浏览器)里找一个你喜欢的位置,右键选择"蓝图类"。
- 弹出的窗口里选择父类,这里直接选最顶部的" Actor ",然后命名为
BP_FirstActor。 - 双击打开 BP_FirstActor,进入蓝图编辑器。这时候左侧面板叫"Components",里面默认只有一个" DefaultSceneRoot "组件。这个组件的角色是给整个 Actor 提供一个"原点",它本身不可见。
- 点击左上角的"添加组件"按钮,搜索并添加一个" Static Mesh "组件。你会发现它自动成了 DefaultSceneRoot 的子级。
- 在右侧详情面板的 Static Mesh 下拉框里,搜索
Cube,选中它。你的方块此时已经在预览窗口里出现了。
到这一步,你已经创建了人生中第一个看得见的 Actor。接下来,编译(Compile)并保存(Save)这个蓝图,把它从内容浏览器里拖到游戏场景中,点击运行(Play),你就能在游戏视口里看到这个方块稳稳地站在原点附近。
这个流程有一个细节很重要:每个 Actor 都需要一个 Root Component(根组件)。你在第 1 步创建后默认就有的 DefaultSceneRoot 就是这个根。我们之所以添加 Static Mesh 组件到它下面,是因为一个 Actor 只能有一个根,根负责定义 Actor 的 Transform(位置、旋转、缩放),子组件全都跟着根走。如果你添加第一个组件时选择了"替换根组件"选项,那 Static Mesh 就会成为新的根。更复杂的情况,比如你想让一个角色身体下有多个零件,你会希望它们统一挂在同一个根下面,这样移动角色时所有零件一起动。
3.2 给 Actor 添加可视化组件:Static Mesh 与材质
方块在预览窗口里显示成灰色,是因为默认材质只是一个基础材质。为了让你的 Actor 更接近真实游戏里的表现,你需要给它指定一个材质。在 Content Browser 里右键,选择"材质",命名M_TestBlock。双击打开材质编辑器,左侧是输出节点,右侧可以添加各种表达式节点。
一个最简单的选择:选中输出节点,在右侧细节面板里把 Base Color 改成你喜欢的颜色。顺便把 Metallic 调到 0,Roughness 调到 0.8 左右,这样它会呈现一种哑光质感,看起来没那么"塑料"。编辑完点击 Apply 保存,回到 BP_FirstActor,把 Static Mesh 组件上的 Material Element 0 换成 M_TestBlock。
大多数初学者在这一步会看到一个现象:预览窗口里方块已经变色了,但拖到场景里看还是灰色的。这并不是你操作错了,而是编辑器视口的实时刷新层级问题。你只需要重新拖入一个新的 BP_FirstActor 到场景,或者把场景里已有的那个删掉再复制一份,就会看到材质正常显示了。
材质这个概念,现在你只需要理解为:它决定了 Actor 表面被光照之后反射成什么颜色和质感。后面学到 PBR 材质系统时你会接触更多参数,但今天知道怎么换颜色就足够了。
3.3 用代码动态生成 Actor:SpawnActor 的两种写法
上面我们通过拖拽把 Actor 放进了关卡,这叫"静态放置"。但实际游戏里大量 Actor 是运行时才出现的:敌人从刷怪点刷新、子弹击中目标后产生爆炸、掉落物从宝箱里弹出。这些都要靠动态生成,UE 里叫 Spawn。
在蓝图中,你只需要使用Spawn Actor from Class节点。它的几个关键输入:
- Class:你要生成哪种 Actor 类型,选 BP_FirstActor。
- Spawn Transform:生成位置和旋转。你可以先用
Make Transform节点连一个位置 (0, 0, 150),表示在原点上方 150 个单位生成。 - Owner:生成它的宿主 Actor,一般可以留空。
接好之后,运行游戏,你会看到方块凭空出现在场景中指定位置。这就是运行时生成一个 Actor 的完整流程。
如果使用 C++,对应代码是这样:
UWorld* World = GetWorld(); if (World) { FVector SpawnLocation(0.f, 0.f, 150.f); FRotator SpawnRotation(0.f, 0.f, 0.f); FActorSpawnParameters SpawnParams; SpawnParams.Owner = this; SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; AActor* SpawnedActor = World->SpawnActor<AActor>(BP_FirstActorClass, SpawnLocation, SpawnRotation, SpawnParams); }注意这里使用了SpawnCollisionHandlingOverride参数。它的意思是:如果要生成的位置被其他物体占用了,引擎会自动调整位置以避免重叠,但如果始终找不到合适位置,仍然强制生成。这个参数是初学者最容易忽略的,因为默认情况下,如果生成点有碰撞阻挡,Actor 会生成失败,但你不一定立刻发现,直到发现敌人没有刷出来。所以我自己写 Spawn 时都会显式指定这个处理方式,避免"为什么我的敌人神秘消失了"这类问题。
4. 生命周期与 Tick:Actor 的出生、心跳与死亡
4.1 BeginPlay 与 Tick 的实际应用场景
Actor 被放进场景,或者运行时通过 Spawn 生成之后,它并不是"死"的。引擎会按照固定的频率对它进行更新。UE 里它表现为三个事件,分别在 Actor 生命周期的不同阶段被调用:
| 事件 | 调用时机 | 典型用途 |
|---|---|---|
| Construction Script | 蓝图被编译、Actor 被放置到场景、属性被修改时 | 根据参数动态搭建组件结构 |
| BeginPlay | 游戏运行时,Actor 进入世界的瞬间 | 初始化血量、绑定事件、查询其他 Actor |
| Tick | 每一帧调用 | 持续更新位置、检测条件、做插值动画 |
新手最容易困惑的点是 Construction Script 和 BeginPlay 的区别。Construction Script 在编辑器里调整参数时就会立刻执行,所以适合做"编辑器里马上能看到变化"的事情,比如根据你设置的楼层高度,动态在预览里生成对应的楼层模型。而 BeginPlay 只在真正运行游戏时才执行,适合做运行时初始化。
Tick 的理解更直接。假设你想让方块在游戏运行时以每秒 90 度的速度绕 Z 轴旋转,在蓝图里就给 Actor 的 Tick 节点加一段逻辑,或者更简单,你直接在 Static Mesh 组件上调用AddLocalRotation,并且把旋转增量乘以Delta Seconds(Tick 自带的参数),这样旋转就和帧率无关了,不管游戏跑 30 帧还是 120 帧,转速都一样。这就是 Tick 里最经典的一个示范。
但这里我要强调一个容易忽略的地方:引擎里每个 Actor 默认都会开 Tick,哪怕它根本不需要每帧更新。当一个关卡里有上千个静态 Actor,每个都开着 Tick,光是空跑一遍 Tick 就会造成不小的 CPU 开销。所以我的习惯是:不在 Tick 里做事的时候,把 Actor 的Start Tick Enabled和Can Ever Tick属性关掉,或者把 Tick 间隔设置成 0.1 秒甚至更长。这种优化在项目规模变大之后会非常明显。
4.2 Actor 的销毁与延迟销毁:谁负责清理垃圾
有出生就有销毁。当你需要移除一个 Actor 时,蓝图中可以直接对目标 Actor 调用Destroy Actor节点。C++ 里对应的是Actor->Destroy()。
但需要注意,Destroy 并不是立刻把内存收回。它只是标记这个 Actor"请求被销毁",引擎会在合适的时机把它从世界中移除,并释放其持有的资源。如果你循环遍历所有 Actor,发现要销毁当前遍历到的 Actor,直接 Destroy 就好,但要避免在遍历的同时还去读取它的属性,因为它可能在下一次 GC 时被回收,继续引用就会崩溃。
另一个非常实用的函数是Set LifeSpan(C++ 里是SetLifeSpan)。它可以告诉引擎:这个 Actor 在存活多少秒后自动销毁。这对爆炸特效这类临时生成的对象非常合适。你生成一个爆炸特效 Actor,设置 LifeSpan 为 2 秒,它会在 2 秒后自己消失,完全不用手动管理。我自己写技能系统时,很多飞弹和弹壳都是用这种方式处理的,省心。
生命周期相关的坑,我后面会在第 5 节集中讲,这里先记住一个核心认知:Actor 的构建、初始化、更新、销毁,这四个阶段都有对应的事件,操作 Actor 时先想清楚现在是哪个阶段,能避免大量 bug。
5. 新手最容易踩的 Actor 相关坑
5.1 Transform 属性与坐标轴混乱
我见过太多新手在创建完 Actor 后,一脸疑惑地问我:为什么我把一个方块放在了场景里某个位置,运行游戏后它却在我的脚下,或者干脆消失了?
这背后往往就是 Transform 弄混了。UE 里的 Transform 包含三个内容:Location(位置)、Rotation(旋转)、Scale(缩放)。你拖进场景的 Actor,它的位置显示的是在世界坐标系的绝对位置,但你在蓝图里修改位置时,要分清楚你是想改"相对位置"(相对父组件的本地空间)还是"世界位置"(关卡坐标系)。
举个例子,你在 BP_FirstActor 的 Static Mesh 组件上把 Relative Location 改成 (0, 0, 100),意思是方块相对 Actor 的根组件往上偏移 100 个单位。你把它拖进场景时,这个偏移依然保留。此时你在世界中看到的方块位置,是" Actor 世界位置 + 组件相对位置"叠加的结果。如果你在蓝图里通过 Tick 移动的是组件相对位置,而你还想让它以世界坐标为准,就会产生奇怪的漂移。
5.2 旋转单位的坑:Degrees 还是 Radians
这是所有从代码写起、尤其是学过一点点数学的开发者最容易纠结的问题。UE 蓝图里的旋转单位是度(Degrees),比如Set Actor Rotation传 (0, 0, 90),表示绕 Z 轴转 90 度。但如果你在 C++ 或者材质蓝图里用三角函数、插值,很多节点用的是弧度(Radians)。你做一个原地转身功能,按直觉写了一个旋转角度值,结果发现角色转得飞快或者几乎不动,多半就是单位写错了。
我自己写逻辑时有一个默认习惯:C++ 里如果涉及旋转参数,先看接口注释里写的是 Degrees 还是 Radians。UE 的FRotator结构体所有分量都是度,而FMath::DegreesToRadians这个函数存在的意义就是让你在两种单位之间转换。蓝图里也是一样,看到一个数值节点连到旋转节点之前,先想清楚它是"度"还是"弧度"。新手阶段这个坑会反复出现,多踩几次自然就记住了。
5.3 在构造函数里创建组件的陷阱
最后一个坑必须单独拿出来讲,因为它牵扯到 Actor 和组件生命周期中非常隐蔽的一段。有些初学者尝试在 C++ 构造函数里直接NewObject创建组件,或者在某些特殊时机动态添加组件,发现运行时组件有时存在有时不存在,属性怎么调都对不上。
正确做法是:C++ 里的组件创建应该放在构造函数里,但必须使用CreateDefaultSubobject这个专用函数,而不是NewObject。因为CreateDefaultSubobject会把组件注册进 Actor 的组件列表,参与后续的初始化、序列化和属性同步。如果你用NewObject创建组件,相当于创建了一个跟 Actor 没有隶属关系的普通对象,它不会被引擎当作 Actor 的组件来管理。
Blueprint 场景中同样有对应的规则:在蓝图编辑器的组件列表里添加组件,是设计时静态添加的方式;运行时动态添加组件需要调用Add Component类节点(比如Add Static Mesh Component)。在蓝图里"动态添加组件"和"设计时添加组件"有一个重要差异,动态添加的组件不会自动拥有一个默认的根组件或者附加位置,你需要自己用Attach To Component把它挂到某个组件下,否则它的位置可能完全不受控制。
这个坑在教程里很少被提到,但实际项目里几乎每个人都会遇到。有一次我在做求生类 Demo 时,想给角色动态挂载一个武器模型,结果忘了设置附加规则,武器模型飞到了地图外面,查了半天才发现是根组件和相对位置没有设置。
6. 实操一遍后,我建议你做的几个小练习
到目前为止你理解了 Actor 是什么、怎么创建、怎么动态生成、怎么管理生命周期,也知道了几个常见的坑。但光看不练,很快会忘。我会带几个新人走这段路,每次都要他们完成三件小事,今天也留给各位:
第一件事:创建一个 BP_RotatingCube 的蓝图 Actor,让它包含一个 Cube 静态网格,并且在 Tick 里绕 Z 轴匀速旋转。做完这一步,你会体会到"组件跟着根一起动"和"组件自身旋转"的区别。
第二件事:写一段 Spawn 逻辑,每按一次键盘上的 1 键,就在角色面前 200 个单位的位置生成一个 BP_RotatingCube,并设置 LifeSpan 为 3 秒。这个练习能让你真正掌握运行时生成 Actor 的流程,同时理解 LifeSpan 的作用。
第三件事:给 BP_RotatingCube 增加一个Hit事件(Static Mesh 组件上有 On Component Hit),当它碰到任何东西时,输出一条日志或者改变材质颜色。这个练习是为了让你理解"事件"在 Actor 中扮演的角色——Actor 里不只有 Tick 在不断运行,它还时刻监听其他系统发来的事件,并做出响应。
这三个练习覆盖了 Actor 的核心能力:组件、生命周期、Tick、动态生成、事件监听。做完后你再回头看 Actor 这个概念,会发现自己已经从"它是名词解释"升级成"我知道怎么用它了"。
以前我刚开始学的时候,总想着先把文档里所有 Actor 相关的类和函数背下来再动手,结果越看越记不住。后来改变策略,直接创建一个小项目,在里面犯各种错,反而成长飞快。很多知识点光靠文字是不够的,亲手把一个方块放在场景里并让它转起来,比读十遍文档都有用。希望今天的这篇内容,能帮你迈出这一步。