================================================================================
Unreal Engine 中 NewObject 与 SpawnActor 的使用指南
================================================================================
一、核心原则
一句话总结:
- NewObject 用于创建 UObject 及其子类(非 Actor 对象)。
- SpawnActor 用于创建 AActor 及其子类(Actor 对象)。
但这只是表面,真正的设计考量涉及网络复制、生命周期管理和世界存在性。
--------------------------------------------------------------------------------
二、详细对比表
特性 | NewObject<UObject> | SpawnActor<AActor>
----------------------|-----------------------------|-----------------------------
创建对象类型 | UObject 及其子类 | AActor 及其子类
是否有世界坐标 | 无 | 有 (Location/Rotation/Scale)
是否显示在场景中 | 不可见 | World Outliner 可见
网络复制 | 不支持(需手动 RPC) | 内置支持 (bReplicates)
生命周期管理 | 需手动管理引用 | BeginPlay/EndPlay 自动管理
Tick 功能 | 无(除非实现接口) | 原生支持
Component 支持 | 不能添加 Component | 可附加 UActorComponent
碰撞检测 | 无 | 支持物理和碰撞
渲染 | 不渲染 | 可渲染(如果有 Mesh)
垃圾回收 | GC 自动管理(需 UPROPERTY) | GC 自动管理
蓝图支持 | 完整支持 | 完整支持
典型用途 | 数据容器、管理器、资源 | 游戏实体、角色、道具
--------------------------------------------------------------------------------
三、使用场景详解
1. 使用 NewObject 的场景
(1) 数据容器 / 配置对象
示例:创建团队显示资源配置
ULyraTeamDisplayAsset* DisplayAsset = NewObject<ULyraTeamDisplayAsset>();
DisplayAsset->TeamName = TEXT("Red Team");
DisplayAsset->TeamColor = FLinearColor::Red;
特点:
- 只存储数据
- 不需要放在世界中
- 不需要 Tick 或渲染
(2) 子系统 / 管理器
示例:创建自定义管理器
UMyGameManager* Manager = NewObject<UMyGameManager>(GetGameInstance());
Manager->Initialize();
特点:
- 全局单例或局部管理器
- 管理其他对象的生命周期
- 通常继承自 UGameInstanceSubsystem 或 UWorldSubsystem
(3) Ability System 的 GameplayEffect
示例:创建临时效果
UGameplayEffect* Effect = NewObject<UGameplayEffect>();
Effect->DurationPolicy = EGameplayEffectDurationType::HasDuration;
Effect->DurationMagnitude = FScalableFloat(5.0f);
特点:
- 临时数据对象
- 被其他系统持有
- 不需要独立存在
(4) UI 控件
示例:创建 Widget
UUserWidget* Widget = CreateWidget<UUserWidget>(GetWorld(), WidgetClass);
注意:CreateWidget 内部调用 NewObject
特点:
- UI 元素不是世界中的 Actor
- 由 UI 系统管理生命周期
(5) AnimInstance / Animation Blueprint
示例:创建动画实例
UAnimInstance* AnimInst = NewObject<UAnimInstance>(Mesh, AnimClass);
Mesh->SetAnimInstance(AnimInst);
特点:
- 附加到其他对象上
- 不是独立的实体
--------------------------------------------------------------------------------
2. 使用 SpawnActor 的场景
(1) 游戏中的实体对象
示例:生成敌人
AEnemyCharacter* Enemy = World->SpawnActor<AEnemyCharacter>(EnemyClass, SpawnLocation, SpawnRotation);
特点:
- 需要在世界中有位置
- 可能需要移动、碰撞、渲染
- 玩家可以看到和交互
(2) 投射物 / 特效
示例:发射子弹
AProjectile* Bullet = World->SpawnActor<AProjectile>(ProjectileClass, MuzzleLocation, MuzzleRotation);
Bullet->SetOwner(Shooter);
特点:
- 需要物理模拟
- 需要碰撞检测
- 需要网络复制(多人游戏)
(3) 拾取物 / 道具
示例:掉落物品
APickupItem* Pickup = World->SpawnActor<APickupItem>(PickupClass, DropLocation);
Pickup->SetItemData(ItemData);
特点:
- 放置在世界中
- 玩家可以走过去拾取
- 可能有旋转动画
(4) 触发器 / 区域
示例:创建任务区域
ATriggerVolume* QuestArea = World->SpawnActor<ATriggerVolume>(TriggerClass, AreaLocation);
QuestArea->OnActorBeginOverlap.AddDynamic(this, &ThisClass::OnEnterQuestArea);
特点:
- 检测玩家进入/离开
- 触发游戏逻辑
- 可能在编辑器中可视化
(5) 信息载体(如 Lyra 的 TeamInfo)
示例:Lyra 代码
ALyraTeamPublicInfo* TeamInfo = World->SpawnActor<ALyraTeamPublicInfo>(PublicTeamInfoClass);
TeamInfo->SetTeamId(TeamId);
为什么用 Actor?
- 需要网络复制到所有客户端
- 需要 BeginPlay/EndPlay 自动注册/注销
- 与 World 生命周期绑定
- 可以被其他 Actor 引用
--------------------------------------------------------------------------------
四、实际案例分析
案例 1:Lyra 的团队系统
错误做法:用 NewObject 创建 TeamInfo
ULyraTeamInfoObject* TeamInfo = NewObject<ULyraTeamInfoObject>();
问题:无法网络复制,客户端不知道团队信息
正确做法:用 SpawnActor 创建 TeamInfo
ALyraTeamPublicInfo* TeamInfo = World->SpawnActor<ALyraTeamPublicInfo>(Class);
优势:自动复制 TeamId 和 DisplayAsset 到所有客户端
案例 2:武器系统
// 武器本身是 Actor(可以掉落在地上)
AWeaponActor* Weapon = World->SpawnActor<AWeaponActor>(WeaponClass, DropLocation);
// 武器的数据配置是 UObject
UWeaponDataAsset* WeaponData = NewObject<UWeaponDataAsset>();
WeaponData->Damage = 50.0f;
WeaponData->FireRate = 10.0f;
// 武器组件是 UActorComponent(附加到 Character)
UWeaponComponent* WeaponComp = NewObject<UWeaponComponent>(Character, WeaponCompClass);
Character->AddComponentByClass(UWeaponComponent::StaticClass(), ...);
三层结构:
1. AWeaponActor - 世界中的实体(SpawnActor)
2. UWeaponDataAsset - 配置数据(NewObject)
3. UWeaponComponent - 行为组件(NewObject + AddComponent)
案例 3:技能系统(GAS)
// 技能定义是 UObject(数据资产)
UGameplayAbility* AbilityDef = LoadObject<UGameplayAbility>(nullptr, TEXT("/Game/Abilities/Fireball"));
// 激活的技能实例是 UObject(由 ASC 管理)
FGameplayAbilitySpec Spec(AbilityDef);
AbilitySystemComponent->GiveAbility(Spec);
// 技能产生的投射物是 Actor
AProjectile* Fireball = World->SpawnActor<AProjectile>(FireballClass, CastLocation);
职责分离:
- UGameplayAbility - 技能逻辑(UObject,不需要在世界中)
- AProjectile - 技能效果(Actor,需要在世界中飞行)
--------------------------------------------------------------------------------
五、常见误区
误区 1:以为 UObject 不能网络复制
事实:UObject 可以通过 Subobject 复制或 RPC 参数传递,但不能像 Actor 那样自动同步属性。如果需要同步 UObject 的属性变化,必须手动调用 RPC。
误区 2:以为 Actor 太重,应该尽量用 UObject
事实:现代 UE 的 Actor 已经很轻量,而且提供了太多便利:网络复制、生命周期管理、Component 系统、调试可见性。只有在你确定不需要这些功能时,才考虑用 UObject。
误区 3:混淆 Component 和 Actor
错误:试图 Spawn 一个 Component
UActorComponent* Comp = World->SpawnActor<UActorComponent>(...); // 编译错误!
正确:Component 必须附加到 Actor
UActorComponent* Comp = NewObject<UActorComponent>(OwnerActor, CompClass);
OwnerActor->AddInstanceComponent(Comp);
Comp->RegisterComponent();
规则:
- AActor 使用 SpawnActor
- UActorComponent 使用 NewObject + AddComponent
- UObject 使用 NewObject
--------------------------------------------------------------------------------
六、快速决策指南
当你需要创建一个对象时,问自己这些问题:
问题 1:它需要有世界坐标(Location/Rotation)吗?
- 是 -> Actor -> SpawnActor
- 否 -> 继续问题 2
问题 2:它需要网络复制属性吗?
- 是 -> Actor -> SpawnActor(或手动 RPC)
- 否 -> 继续问题 3
问题 3:它需要 Tick 每帧更新吗?
- 是 -> Actor -> SpawnActor(或实现 FTickableGameObject)
- 否 -> 继续问题 4
问题 4:它需要附加 Component 吗?
- 是 -> Actor -> SpawnActor
- 否 -> 继续问题 5
问题 5:它只是数据容器或管理器吗?
- 是 -> UObject -> NewObject
- 否 -> 重新思考设计
--------------------------------------------------------------------------------
七、代码模板
1. NewObject 模板
// 基础用法
UMyObject* Obj = NewObject<UMyObject>(Outer, MyClass);
// 常见 Outer 选择
UMyObject* Obj1 = NewObject<UMyObject>(this); // 当前对象拥有
UMyObject* Obj2 = NewObject<UMyObject>(GetWorld()); // World 拥有
UMyObject* Obj3 = NewObject<UMyObject>(GetGameInstance()); // GameInstance 拥有
UMyObject* Obj4 = NewObject<UMyObject>(nullptr, MyClass); // transient(临时)
2. SpawnActor 模板
// 基础用法
AMyActor* Actor = World->SpawnActor<AMyActor>(ActorClass, Location, Rotation);
// 高级用法
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = GetInstigator();
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AMyActor* Actor = World->SpawnActor<AMyActor>(ActorClass, Transform, SpawnParams);
--------------------------------------------------------------------------------
八、总结
场景 | 推荐方式 | 原因
----------------------|---------------|----------------------------------
游戏实体 | SpawnActor | 需要位置、碰撞、渲染
数据配置 | NewObject | 纯数据,无需世界存在
管理器/子系统 | NewObject | 全局单例,管理其他对象
需要网络复制的对象 | SpawnActor | 内置复制机制
UI 控件 | CreateWidget | UI 系统管理(内部 NewObject)
Component | NewObject | 必须附加到 Actor
临时效果数据 | NewObject | 短暂存在,被其他对象持有
触发器/区域 | SpawnActor | 需要在世界中检测重叠
记住这个黄金法则:
如果它需要在游戏世界中"存在"(有位置、能被看到、能交互),就用 SpawnActor。
如果它只是"数据"或"逻辑",就用 NewObject。
Lyra 的 ALyraTeamInfoBase 选择 Actor 是因为它需要在网络中同步团队信息,这是典型的"需要在世界中存在"的场景(虽然不是物理存在,但是逻辑存在)。