Unreal ARPG笔记

今天我们来拆解一个具体的基于UE5实现的ARPG Demo的内容来了解一个ARPG具体实现需要的内容。

这是一个Github上的开源项目,地址如下:Mecha-NOX/Echoes-of-the-Lost-Sands: A Soulslike RPG developed in UE5 featuring dynamic melee combat, diverse environments, and AI-driven enemies

我们先大体上把这个项目拆解成这么几个部分:

  1. 角色与战斗
  2. 敌人AI与Boss
  3. 物品与交互
  4. 环境与关卡
  5. 用户界面与体验
  6. 输入与架构支持
  7. 存档与数据管理

接下来我们来一个部分一个部分地拆解,因为这似乎是我的第一个Unreal引擎实现的Demo,所以我会添加很多UE相关的内容以及和Unity的对比。

我们先针对UE和Unity的区别有个大概的概念:

在我看来,UE和Unity最大的差异在于两个点:在底层的内容都是基于C++实现的前提下,UE为了保证性能向上封装时依然使用了C++进行封装而Unity为了方便性以及安全性考虑选择了C#进行封装,这导致了UE非常难用(比如内存管理)但是得到了极致的性能,而Unity非常好用的同时难以实现顶级的视觉效果;第二个点则是UE的蓝图系统使得UE比起Unity来说有着更强大的可视化效果,而蓝图的强大可视化也方便了诸如行为树、黑板、状态机等功能的具体部署而Unity这方面没有做的那么好。

角色与战斗

角色移动与操作

聊到角色当然得让角色先动起来,别的不说,基本的移动是必须的,然后作为一个强调动作元素的游戏,往往会多做一个锁定的功能。

具体来说,我们利用UE的Enhanced Input System来实现输入的映射,将具体的输入映射到诸如具体Move、Look、Dodge、LockTarget函数。

这里需要补充一下关于UE特有的Enhanced Input System组件。

Enhanced Input System(增强输入系统)是UE5引入的新一代输入管理框架,替代了传统的Input Mapping和Input Action系统。它提供了更灵活、数据驱动、易扩展的输入处理方式,适用于现代游戏对多平台、多设备、复杂操作的需求。

输入映射上下文(Input Mapping Context,IMC)本质上是一组输入配置的集合。通过激活不同的IMC,同一个按键可以在不同的游戏状态下触发不同的Input Action,只有当前激活的IMC才会响应输入。如果多个IMC同时激活,系统会根据优先级决定哪个IMC先处理输入。因此,IMC的最大作用是让同一个按键在不同场景下拥有不同的功能,避免输入冲突,极大提升了输入管理的灵活性和可扩展性。

回到我们的角色移动部分来,我们以角色移动的实现举例:

玩家通过键盘(WASD)或手柄控制角色在世界中前后左右移动。

cpp 复制代码
void Move(const FInputActionValue& Value);
...
...
EnhancedInput->BindAction(MovementAction, ETriggerEvent::Triggered, this, &ASlashCharacter::Move);
...
...
void ASlashCharacter::Move(const FInputActionValue& Value)
{
    if (ActionState != EActionState::EAS_Unoccupied) return; // 只有在空闲状态下才能移动
    const FVector2D MovementVector = Value.Get<FVector2D>();
    if (Controller)
    {
        const FRotator ControlRotation = GetControlRotation();
        const FRotator YawRotation(0.f, ControlRotation.Yaw, 0.f);

        const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
        AddMovementInput(ForwardDirection, MovementVector.Y);
        const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
        AddMovementInput(RightDirection, MovementVector.X);
    }
}

我们写好一个具体的Move函数之后调用EnhancedInput中的BindAction函数就可以实现一个具体的输入绑定了。

这里首先补充一下BindAction的用法:

cpp 复制代码
BindAction(
    UInputAction* Action,           // 要绑定的输入动作(资源)
    ETriggerEvent TriggerEvent,     // 触发类型(如按下、松开、持续等)
    UObject* Object,                // 回调函数所属对象(通常是this)
    函数指针                        // 回调函数
)

然后是这个Move函数的含义: 这个函数用于根据玩家的输入控制角色在世界中的前后和左右移动,并确保只有在角色处于空闲状态时才能移动。它会根据当前摄像机的朝向,具体来说,通过获取摄像机的Yaw朝向,计算出世界坐标下的前后和左右方向,然后将玩家输入的二维向量分别映射到这两个方向,实现角色始终相对于摄像机视角的移动。

然后可以看到Move继承自ASlashCharacter类,ASlashCharacter 类继承自 ABaseCharacter 类,而 ABaseCharacter 又继承自 Unreal Engine 的 ACharacter 类。

  • ACharacter:UE自带的角色基类,包含了角色移动、动画、碰撞等最基础的功能。
  • ABaseCharacter:自定义的基础角色类,封装了所有角色(包括玩家和敌人)都需要的通用逻辑,比如受击、死亡、属性组件等。
  • ASlashCharacter:玩家专属角色类,扩展了玩家特有的功能,如输入响应、拾取物品、锁定目标等。

这里说点比较宏观的内容:UE自带了哪些常用的类?

攻击与连击系统

我们的攻击系统比较复杂,支持普通攻击、连击(Combo)、蓄力攻击等多种攻击方式。

普通攻击是指玩家短按攻击键(如鼠标左键)时触发的攻击。实现流程为:按下攻击键时先检测角色是否可攻击,若可以则播放普通攻击动画(动画蒙太奇的第一个Section),在动画的特定帧通过AnimNotify启用武器碰撞,检测命中并造成伤害,动画结束后重置状态,等待下一次攻击输入。

连击是指玩家在攻击动画的"连击窗口"内连续点击攻击键,实现连续攻击。具体做法是:将攻击动画蒙太奇分为多个Section(如Attack1、Attack2、Attack3),在每个Section的合适帧插入AnimNotify以开启"连击窗口",玩家在窗口内再次按下攻击键时记录连击输入,动画事件检测到连击输入后立即切换到下一个Section,实现流畅连招。若连击结束或未输入,动画回到初始状态。

蓄力攻击则是玩家长按攻击键,松开时释放强力攻击。实现流程为:按下攻击键时记录蓄力开始时间并进入蓄力准备状态,持续按住时角色可播放蓄力准备动画或特效,松开攻击键时计算蓄力时长,根据时长决定蓄力等级和伤害,最后播放对应的蓄力攻击动画并造成更高伤害。

这里会有一个问题:我们如何在输入映射上区分三种攻击呢?和技能不同,我们的这三种攻击往往共享一个键(比如鼠标左键)。

  1. Input Action设计
  • 创建一个"攻击"Input Action(如AttackAction)。
  • 在AttackAction的Triggers中同时添加:
  • Tap Trigger:用于检测短按(普通攻击/连击)。
  • Hold Trigger:用于检测长按(蓄力攻击),可设置Hold Time(如0.5秒)。
  1. 代码绑定
cpp 复制代码
cpp

Apply

EnhancedInput->BindAction(AttackAction, ETriggerEvent::Started, this, &ASlashCharacter::OnAttackStarted);

EnhancedInput->BindAction(AttackAction, ETriggerEvent::Completed, this, &ASlashCharacter::OnAttackReleased);
  1. 逻辑处理
  • OnAttackStarted:记录按下时间,准备进入蓄力状态。
  • OnAttackReleased:
  • 判断按下时长是否小于Hold Time:
  • 小于Hold Time:执行普通攻击或连击逻辑(根据当前动画状态和连击窗口判断)。
  • 大于Hold Time:执行蓄力攻击逻辑(根据蓄力时长决定伤害和动画)。
  1. 系统自动区分
  • Enhanced Input System会根据Trigger配置自动区分短按和长按,开发者只需在回调函数中根据时长和状态判断,保证逻辑不会冲突。

总结来说就是通过Enhanced Input的Tap和Hold Trigger自动区分,代码中根据时长和状态判断,保证三种输入互不冲突且响应流畅。

受击与死亡

攻击方的"攻击"主要体现在播放攻击动画和在关键帧启用武器的碰撞体,而真正的伤害判定和反馈逻辑则全部由受击方的受击函数来实现。当武器的碰撞体与角色发生碰撞时,动画事件会触发碰撞检测,系统会判断对方是否具备可被攻击的条件(如实现了受击接口或拥有特定Tag)。如果可以受击,就会调用该角色的受击函数。受击函数内部负责播放受击动画、音效、粒子特效,并通过属性组件扣除生命值,同时判断是否进入死亡状态。这样,攻击方只负责动作和碰撞的触发,所有具体的受伤和死亡反馈都集中在受击方的逻辑中实现,保证了系统的清晰和可扩展性。

受到攻击时,攻击方会调用被攻击角色的 GetHit_Implementation 函数(该函数在 ABaseCharacter 类中实现)。

在 GetHit_Implementation 中:

  • 首先判断角色是否还活着(IsAlive())。
  • 如果活着,则播放受击动画(如方向性受击反应)、受击音效和粒子特效。
  • 同时会调用属性组件(UAttributeComponent)的 ReceiveDamage 方法,扣除生命值。
cpp 复制代码
void ABaseCharacter::GetHit_Implementation(const FVector& ImpactPoint, AActor* Hitter)
{
    if (IsAlive() && Hitter)
    {
        DirectionalHitReact(Hitter->GetActorLocation()); // 播放受击动画
    }
    else Die(); // 如果已死亡则直接进入死亡流程

    PlayHitSound(ImpactPoint);      // 播放受击音效
    SpawnHitParticles(ImpactPoint); // 播放受击粒子
}

具体如何实现方向性受击反应?UAttributeComponent又是什么组件?ReceiveDamage具体怎么实现的?

cpp 复制代码
void ABaseCharacter::DirectionalHitReact(const FVector& ImpactPoint)
{
    FVector Forward = GetActorForwardVector();
    FVector ToHit = (ImpactPoint - GetActorLocation()).GetSafeNormal();
    float DotProduct = FVector::DotProduct(Forward, ToHit);

    // 判断前后左右
    if (DotProduct > 0.7f)
        PlayHitReactMontage("HitFront");
    else if (DotProduct < -0.7f)
        PlayHitReactMontage("HitBack");
    else
    {
        float CrossZ = FVector::CrossProduct(Forward, ToHit).Z;
        if (CrossZ > 0)
            PlayHitReactMontage("HitLeft");
        else
            PlayHitReactMontage("HitRight");
    }
}

在 GetHit_Implementation 函数中,传入了攻击者的位置(Hitter->GetActorLocation())。通过计算攻击者与被攻击者之间的相对方向(攻击向量和被攻击者朝向点积判断前后,叉积Z分量判断左右),判断攻击来自哪个方位。

UAttributeComponent 是一个自定义的Actor组件(UActorComponent的子类),用于管理角色的各种属性,如生命值(Health)、体力(Stamina)、灵魂、金币等。

ReceiveDamage(float Damage) 是 UAttributeComponent 的成员函数,用于处理角色受到的伤害。

cpp 复制代码
void UAttributeComponent::ReceiveDamage(float Damage)
{
    Health = FMath::Clamp(Health - Damage, 0.f, MaxHealth);
}

clamp函数就是"限制数值在区间内",防止出现不合理的数值溢出或下溢。

属性与成长

属性和成长系统通过UAttributeComponent组件实现,负责所有属性的存储、变化和成长逻辑。

属性这一块其实没啥好说的,就是写一个类,然后定义一系列需要管理的属性类,写明白具体各个属性和UI的协同,各个属性的添加或者扣除的逻辑方便其他的类来调用。

cpp 复制代码
// 扣除生命值
void UAttributeComponent::ReceiveDamage(float Damage)
{
    Health = FMath::Clamp(Health - Damage, 0.f, MaxHealth);
}

// 扣除体力
void UAttributeComponent::UseStamina(float StaminaCost)
{
    Stamina = FMath::Clamp(Stamina - StaminaCost, 0.f, MaxStamina);
}

// 恢复体力
void UAttributeComponent::RegenStamina(float DeltaTime)
{
    Stamina = FMath::Clamp(Stamina + StaminaRegenRate * DeltaTime, 0.f, MaxStamina);
}

// 增加灵魂
void UAttributeComponent::AddSouls(int32 NumberOfSouls)
{
    Souls += NumberOfSouls;
}

升级(Level Up)系统的核心实现思路就是:当角色升级时,根据设定的成长公式或倍率,提升各项属性,并同步到角色的属性组件中。

武器装备与切换

关于武器和装备,我们主要考虑这么几个部分:如何拾取武器,如何装备武器,如何切换武器,以及其背后带来的属性变化。

cpp 复制代码
// 武器蓝图或C++类中的碰撞事件
void OnOverlapBegin(AActor* OverlappedActor, AActor* OtherActor)
{
    if (OtherActor->HasTag("Player"))
    {
        ShowPickupUI(); // 弹出"按E拾取"提示
        if (PlayerPressedE())
        {
            Player->AddWeaponToInventory(this); // 添加到玩家背包或装备栏
            Destroy(); // 销毁场景中的武器实例
        }
    }
}

当玩家靠近武器时,通过碰撞检测触发拾取逻辑,弹出拾取提示并在玩家按下交互键后将武器添加到背包或装备栏,并销毁场景中的武器实例。

cpp 复制代码
// 玩家装备武器时
void EquipWeapon(AWeapon* Weapon)
{
    FName SocketName = "hand_r_socket"; // 右手插槽
    Weapon->AttachToComponent(GetMesh(), SocketName); // 绑定到角色骨骼的Socket
    EquippedWeapon = Weapon;
    UpdatePlayerStats(Weapon->GetWeaponStats()); // 根据武器属性更新玩家属性
}

装备武器时,将武器的Mesh绑定到角色骨骼的指定Socket(如右手),实现武器与角色动画同步,并根据武器属性实时更新玩家的相关属性。

cpp 复制代码
// 玩家切换武器时
void SwitchWeapon(AWeapon* NewWeapon)
{
    if (EquippedWeapon)
    {
        EquippedWeapon->DetachFromActor(); // 卸下当前武器
    }
    EquipWeapon(NewWeapon); // 装备新武器
    PlaySwitchWeaponEffect(); // 播放切换动画/音效
}

切换武器时,先卸下当前装备的武器,再将新武器绑定到Socket,并播放相应的切换反馈,确保装备和属性的实时同步与视觉表现的流畅。

而关于切换武器造成的数据变化,我们的每个武器类的具体实现就很重要了:

在本项目中,武器类通常是AWeapon,它继承自AItem,并扩展了装备、碰撞、伤害等功能。

cpp 复制代码
// Weapon.h
class AWeapon : public AItem
{
    // ...
    UPROPERTY(EditAnywhere, Category = "Weapon Properties")
    float Damage = 20.f; // 武器伤害

    UPROPERTY(EditAnywhere, Category = "Weapon Properties")
    USoundBase* EquipSound; // 装备音效

    UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
    UBoxComponent* WeaponBox; // 武器碰撞体

    // 其他如特效、攻击范围、攻击速度等属性
    // ...
};

一般的做法是:将武器的所有数值和配置集中存储在一个数据资产(Data Asset)中,如UWeaponDataAsset,每个武器蓝图实例只需引用一个数据资产,便于批量管理和扩展。

cpp 复制代码
// WeaponDataAsset.h
UCLASS()
class UWeaponDataAsset : public UPrimaryDataAsset
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere)
    float Damage;

    UPROPERTY(EditAnywhere)
    float AttackSpeed;

    UPROPERTY(EditAnywhere)
    USoundBase* EquipSound;

    // 其他属性
};

具体的读取方法:

cpp 复制代码
// Weapon.h
UCLASS()
class AWeapon : public AItem
{
    GENERATED_BODY()
public:
    // ...
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon Properties")
    UWeaponDataAsset* WeaponData;
    // ...
};
cpp 复制代码
// Weapon.cpp
void AWeapon::BeginPlay()
{
    Super::BeginPlay();

    if (WeaponData)
    {
        // 读取数据资产中的属性
        Damage = WeaponData->Damage;
        AttackSpeed = WeaponData->AttackSpeed;
        EquipSound = WeaponData->EquipSound;
        // 你可以将这些值赋给本地变量,或者直接用WeaponData->Damage等
    }
}

这里我补充一下UE中关于蓝图和C++脚本的区别:

蓝图(Blueprint)是Unreal Engine(UE)独有的可视化脚本系统,本质上是对C++类的可视化扩展和包装。

如何理解这里的编译执行和解释执行呢?

这里补充一下Lua和C#:

C#的执行方式确实比较特别。它不是像C++那样直接把源码编译成机器码,而是先将源码编译成中间语言(IL,Intermediate Language),然后在程序运行时由虚拟机(如.NET CLR或Mono)通过JIT(Just-In-Time)即时编译,把中间语言动态转换成机器码执行。这样做的好处是,C#的运行效率比纯解释型语言(如Lua、蓝图)高很多,同时又保留了跨平台和一定的灵活性。缺点是首次运行某段代码时会有JIT编译的开销,但之后如果代码未变,运行速度接近编译型语言。

而Lua和UE蓝图则属于典型的解释执行:它们通常把源代码或节点转换成中间格式(字节码),然后由虚拟机逐行(Lua)或逐节点(蓝图)解释执行。这样虽然灵活、易于热更新,但运行效率比C#和C++低。

既然都说到这里了,我们为何不学习一下Mono具体是什么?

这里我们又引出一个问题了:Mono内存是什么?不要问我为什么问这个问题,因为我面试刚被提问。

那既然都了解了Unity的内存管理机制,怎么能不来了解了解UE的呢?

  • UObject GC:定期扫描所有UObject引用树,自动回收无主对象,防止内存泄漏。
  • 资源自动管理:资源对象(如UTexture、USoundWave等)由UE资源系统自动加载和释放。
  • 手动管理:非UObject的C++对象、第三方库对象等,需开发者手动释放。

Unreal Engine的内存分层主要包括:UObject GC层(自动回收)、原生C++内存(手动管理)、资源内存(自动/手动管理)等。它不是纯C++的"野生"内存管理,而是结合了反射、GC和资源系统的现代化内存管理体系,既有高性能也有较好的安全性。

一句话总结:UE会自动回收所有UObject及其资源对象的内存(通过GC和资源管理系统),而非UObject的普通C++对象和第三方内存则需要你手动管理和释放。

敌人AI与Boss

敌人AI需求的功能无非巡逻、感知玩家、状态机切换、攻击动画等等,通常来说会用行为树实现。

这里我想先介绍一下关于状态机和行为树的区别:

在UE中具体如何实现状态机和行为树呢?

答案当然是有现成的。

在Unreal Engine(UE)中,状态机通常通过动画蓝图的可视化状态机系统实现角色动画的切换(如待机、奔跑、攻击、死亡等),而游戏逻辑中的状态机则多用枚举变量配合蓝图或C++代码实现状态的判断与切换;行为树则通过内置的Behavior Tree和Blackboard资源,在可视化编辑器中拖拽节点搭建AI决策逻辑,并由AIController加载和运行,支持复杂的AI行为、条件判断和优先级控制,两者都可以结合蓝图和C++进行自定义扩展。

如何抉择呢?

这个项目中,我们的普通敌人使用简单的状态机而BOSS则使用行为树实现。

状态机的内容我就不再赘述,我就随便拿一部分内容举例:

cpp 复制代码
// 状态管理
void AEnemy::Tick(float DeltaTime)
{
    if (IsDead()) return;
    if (EnemyState > EEnemyState::EES_Patrolling)
    {
        CheckCombatTarget(); // 追击或攻击
    }
    else
    {
        CheckPatrolTarget(); // 巡逻
    }
}

// 感知玩家
void AEnemy::PawnSeen(APawn* SeenPawn)
{
    EnemyState = EEnemyState::EES_Chasing;
    CombatTarget = SeenPawn;
}

// 攻击逻辑
void AEnemy::Attack()
{
    if (CanAttack())
    {
        PlayAttackMontage();
        EnemyState = EEnemyState::EES_Attacking;
    }
}

// 受击与死亡
void AEnemy::GetHit_Implementation(const FVector& ImpactPoint, AActor* Hitter)
{
    ShowHealthBar();
    HandleDamage(DamageAmount);
    if (!IsAlive())
    {
        Die();
    }
}

状态机会在在Tick函数中根据状态执行不同逻辑,这里的Tick函数有必要补充一下:

当然,既然聊到生命周期函数,我们当然不能绕开一个特殊的存在------协程

很遗憾UE没有Unity类似的协程机制,但是UE可以直接设置定时器或者通过异步任务来执行异步操作。

补充一下这个执行顺序的问题,问就是刚被面试问到然后不知道。

然后是具体行为树的实现,这个东西作为一个可视化编辑的组件,用文字显然不太好陈述,我这里放一个官方的教程链接,大家自行查看吧:虚幻引擎行为树快速入门指南 | 虚幻引擎 5.6 文档 | Epic Developer Community

然后这里我还需要再补充一个内容,关于UE的敌人的感知系统:

AI感知系统是Unreal Engine为AI角色提供的一套"感知世界"的机制,它让AI能够像人一样"看到"、"听到"或"感受到"玩家和其他目标,从而实现如发现玩家、追击、躲避、响应声音等智能行为,大大简化了复杂AI的开发流程。

其用法如下:

其执行流程如下:

  • 感知组件会在每一帧自动检测周围环境,根据配置的感知类型(如Sight、Hearing)扫描目标。
  • 视觉感知会检测所有在视野范围和角度内、未被遮挡的目标。
  • 听觉感知会检测范围内的声音事件(如玩家奔跑、开枪等)。
  • 检测到目标后,感知系统会生成"感知刺激"(Stimulus),并通过回调函数通知AIController或角色。
  • AI可以根据感知到的刺激类型、强度、持续时间等信息,做出相应的决策(如追击、警觉、逃跑等)。

在Unreal Engine中,只需给敌人添加感知系统(如AIPerceptionComponent),并给玩家等目标添加刺激源(如AIPerceptionStimuliSourceComponent),就能让敌人自动感知玩家。当敌人感知到刺激源时,感知系统会生成刺激(Stimulus),并通过回调函数(如OnTargetPerceptionUpdated)通知AI。你可以在回调中更新黑板(Blackboard)中的变量(如"TargetActor"),行为树会根据黑板变量自动切换到追击、攻击等相关节点,实现完整的智能敌人AI感知与决策流程。

物品与交互

我们所有的物品继承自一个自定义的类AItem类,而AItem类继承自AActor类。

物品具体的交互逻辑正如我之前所言,通过碰撞检测来做。

cpp 复制代码
// 物品拾取
void AItem::OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, ...)
{
    IPickupInterface* PickupInterface = Cast<IPickupInterface>(OtherActor);
    if (PickupInterface)
    {
        PickupInterface->SetOverlappingItem(this); // 玩家可拾取
    }
}

// 玩家按下E键
void ASlashCharacter::EKeyPressed()
{
    if (OverlappingItem)
    {
        OverlappingItem->OnPickedUp(this); // 执行拾取逻辑
    }
}

这里又不得不提一下Unity和UE的碰撞检测相关的函数对比了:

大致的角度上来说,Unity中的Collision就是UE中的Hit,Unity中的Trigger就是UE中的Overlap。

环境与关卡

具体如何设计环境和关卡我不介绍,那是策划的内容,我在这里主要介绍的是UE中相关的组件。

首先,UE的每一个场景就是一个关卡(.umap文件),UE支持关卡的流式加载(Level Streaming):

开发者可以在主关卡中通过蓝图节点(如"Load Stream Level"和"Unload Stream Level")或C++代码(如UGameplayStatics::LoadStreamLevel)主动控制子关卡的加载和卸载。通常在玩家靠近某个区域、触发某个事件或满足特定条件时,动态加载需要的子关卡,离开时再卸载,适合需要精确控制关卡内容的场景切换和事件驱动型关卡管理。

又或者在主关卡中放置Level Streaming Volume体积,并将其与子关卡关联。当玩家进入该体积区域时,关联的子关卡会自动加载,离开时自动卸载。无需手动编写逻辑,适合大世界、房间、地牢等区域化场景的无缝切换,极大简化了大地图的内容管理和性能优化。

更详细的关卡流式加载的内容可以看这篇知乎:(99+ 封私信 / 80 条消息) 虚幻UE5基础知识-关卡流送(Level Streaming) - 知乎

我们还可以实现Packed Level Instance,将关卡作为实例化对象嵌入到主世界,实现地牢、房间等模块化设计。

虚幻引擎中的关卡实例化 | 虚幻引擎 5.6 文档 | Epic Developer Community

最后的世界分区嘛,我其实都已经介绍过了,这个内容比较复杂,我这里多放几篇文献:

虚幻引擎中的世界分区 | 虚幻引擎 5.6 文档 | Epic Developer Community

(99+ 封私信 / 80 条消息) UE5 World Partition不完全指南 - 知乎

然后是环境方面的组件,这个部分我个人觉得不太涉及到代码,大家自行查阅吧。

用户界面与体验

UE中有专门的针对UI的蓝图类:UMG(Unreal Motion Graphics),所有UI界面均通过UMG蓝图(Widget Blueprint)实现,支持可视化编辑、动画、交互等。

我们先来了解一下这个UMG类:

输入与架构支持

输入我们之前已经聊过了,就是我们的Enhanced Input System,基于输入映射上下文,更灵活也更具有扩展性。

这里的架构支持指的则是我们这个项目中集成了Action System与GameplayTags这两个架构。

在虚幻引擎中使用Gameplay标签 | 虚幻引擎 5.5 文档 | Epic Developer Community

存档与数据管理

说到具体数据的存储和存档系统的实现,Unity和UE的方法是否相同呢?

我们来专门聊聊UE实现存档的方法:

总结来说就是,USaveGame子类+UPROPERTY实现动态存档,Data Asset用于静态配置,.ini文件用于简单设置和参数。

相关推荐
Yawesh_best12 小时前
告别系统壁垒!WSL+cpolar 让跨平台开发效率翻倍
运维·服务器·数据库·笔记·web安全
Ccjf酷儿14 小时前
操作系统 蒋炎岩 3.硬件视角的操作系统
笔记
习习.y15 小时前
python笔记梳理以及一些题目整理
开发语言·笔记·python
在逃热干面15 小时前
(笔记)自定义 systemd 服务
笔记
DKPT17 小时前
ZGC和G1收集器相比哪个更好?
java·jvm·笔记·学习·spring
QT 小鲜肉18 小时前
【孙子兵法之上篇】001. 孙子兵法·计篇
笔记·读书·孙子兵法
星轨初途19 小时前
数据结构排序算法详解(5)——非比较函数:计数排序(鸽巢原理)及排序算法复杂度和稳定性分析
c语言·开发语言·数据结构·经验分享·笔记·算法·排序算法
QT 小鲜肉20 小时前
【孙子兵法之上篇】001. 孙子兵法·计篇深度解析与现代应用
笔记·读书·孙子兵法
love530love1 天前
【笔记】ComfUI RIFEInterpolation 节点缺失问题(cupy CUDA 安装)解决方案
人工智能·windows·笔记·python·插件·comfyui
愚戏师1 天前
MySQL 数据导出
数据库·笔记·mysql