【UE源码精读-ActionRPG】伤害管线:从捕获到扣血

TOC

上一篇属性系统精读以一句"至于 Damage → Health 的完整换算,是下一篇的主角"收尾。现在轮到这条链路登场:从一次攻击能力激活,到属性系统最终把血量扣掉,中间经过了哪些层、每层的职责是什么。ActionRPG 把这条链路设计得出奇地清晰,读完它,你对 GAS 执行计算的整个机制都会有质的提升。


一、链路全景:六层调用

先用一张图把全程六层串起来,再逐层剖析:

bash 复制代码
能力激活(GA_Melee_Base 蓝图)
  │  MakeEffectContainerSpec → ApplyEffectContainerSpec
  ▼
GameplayEffect 施加(含 URPGDamageExecution 作为 ExecutionCalculation)
  │  Execute_Implementation 被调用
  ▼
URPGDamageExecution::Execute_Implementation
  │  捕获属性 → 套公式 → 输出 DamageDone 到 Damage 属性
  ▼
PostGameplayEffectExecute(URPGAttributeSet)
  │  检测 Damage 属性被改 → 读出值 → 清零 → 扣 Health
  ▼
ARPGCharacterBase::HandleDamage
  │  C++ 中间层,组装参数
  ▼
OnDamaged(BlueprintImplementableEvent)
    蓝图中实现:播放受击特效、判断死亡等

前两层(能力激活与效果施加)是 GAS 基础的话题,下一篇会展开。本文专注后四层URPGDamageExecutionPostGameplayEffectExecuteHandleDamageOnDamaged


二、URPGDamageExecution:属性捕获与伤害公式

URPGDamageExecution 继承自 UGameplayEffectExecutionCalculation,是 GAS 里自定义"执行计算"的标准写法。它的任务是:拿到攻击者和被攻击者的属性,按公式算出伤害,输出给属性系统。

Modifier 与 Execution 的区别 GE 有两种修改属性的方式:ModifierExecution 。Modifier 是"一个 Modifier 对应一个 Attribute"的简单运算(加/乘/除/覆盖),适合"加 50 攻击力"这类直接修改。Execution(UGameplayEffectExecutionCalculation)则可以一次读取多个属性、做复杂运算、输出到多个属性 ,适合"用攻击力和防御力计算最终伤害"这类跨属性的公式计算。ActionRPG 的伤害选择了 Execution 路线------因为伤害公式需要同时读取 DamageAttackPowerDefensePower 三个属性,Modifier 无法做到。

2.1 RPGDamageStatics:声明要用的属性

计算之前,先要告诉 GAS "我要读哪些属性"。这用一个叫 RPGDamageStatics 的 local struct 完成:

cpp 复制代码
struct RPGDamageStatics
{
    DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower);
    DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);
    DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);

    RPGDamageStatics()
    {
        // 目标的防御力:不快照(实时读)
        DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false);

        // 施法者的攻击力:快照(用发动时的值)
        DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, true);

        // 施法者的基础伤害:快照
        DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, Damage, Source, true);
    }
};

static const RPGDamageStatics& DamageStatics()
{
    static RPGDamageStatics DmgStatics;
    return DmgStatics;
}

这里出现两个宏:DECLARE_ATTRIBUTE_CAPTUREDEF 在 struct 内声明一个"捕获定义"成员,DEFINE_ATTRIBUTE_CAPTUREDEF 在构造函数里把它和具体的属性绑定,同时指定:

参数 含义
URPGAttributeSet 属性所在的 AttributeSet 类
DefensePower / AttackPower / Damage 属性名
Target / Source 从哪一方捕获
false / true 是否快照(Snapshot)

然后在构造函数里把这三个捕获定义注册进去:

cpp 复制代码
URPGDamageExecution::URPGDamageExecution()
{
    RelevantAttributesToCapture.Add(DamageStatics().DefensePowerDef);
    RelevantAttributesToCapture.Add(DamageStatics().AttackPowerDef);
    RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
}

2.2 快照 vs 不快照:抛射物场景的设计意图

快照(Snapshot = true) :创建 GameplayEffectSpec 那一刻,把属性值复制进去,之后不再跟踪变化。

不快照(Snapshot = false):每次执行计算时实时从目标读。

注释里有一个非常贴切的抛射物例子:

我们发射一颗火球。GE Spec 在发射时 创建------此时用发射者的 AttackPower 做快照。火球飞行途中,发射者的攻击力可能因为某个 Buff 下降了。当火球命中目标时,ExecutionCalculation 执行:它用的是快照里的攻击力(发射时),而不是命中时的实时值。这符合"攻击时锁定伤害"的直觉。

反观 DefensePower:它设置成不快照,因为我们希望用命中时目标的实际防御力来减伤------目标如果在火球飞行途中主动使用了防御技能,盾应该有效。

这两条规则看似简单,却决定了大量 RPG 数值交互的正确性。

2.3 伤害公式

Execute_Implementation 是实际运行的入口:

cpp 复制代码
void URPGDamageExecution::Execute_Implementation(
    const FGameplayEffectCustomExecutionParameters& ExecutionParams,
    OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
    FAggregatorEvaluateParameters EvaluationParameters;
    EvaluationParameters.SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
    EvaluationParameters.TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

    float DefensePower = 0.f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
        DamageStatics().DefensePowerDef, EvaluationParameters, DefensePower);
    if (DefensePower == 0.0f) DefensePower = 1.0f;  // 防止除零

    float AttackPower = 0.f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
        DamageStatics().AttackPowerDef, EvaluationParameters, AttackPower);

    float Damage = 0.f;
    ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
        DamageStatics().DamageDef, EvaluationParameters, Damage);

    // -------------------------------------------------------
    //  Damage Done = Damage * AttackPower / DefensePower
    // -------------------------------------------------------
    float DamageDone = Damage * AttackPower / DefensePower;
    if (DamageDone > 0.f)
    {
        OutExecutionOutput.AddOutputModifier(
            FGameplayModifierEvaluatedData(
                DamageStatics().DamageProperty,
                EGameplayModOp::Additive,
                DamageDone));
    }
}

公式一目了然:

bash 复制代码
真实伤害 = 基础伤害 × 攻击力 / 防御力

几个细节:

  • AttemptCalculateCapturedAttributeMagnitude:这是"尝试"读取捕获的属性聚合值,带了标签过滤------拥有某些 Tag 的 Buff 才生效。比如"只对中毒状态目标有效的减防"可以靠标签做区分。
  • 输出 是调用 OutExecutionOutput.AddOutputModifier,用 EGameplayModOp::AdditiveDamage 属性上 一个值。注意这里的"加"加的是DamageProperty(Damage 属性的反射句柄)------也就是说,Execute_Implementation 的输出,是对源 ASC 上的 Damage 属性叠加一个增量 ,而不是直接扣目标血量。这个增量进入 GAS 的修饰器管线,接着触发 PostGameplayEffectExecute

三、PostGameplayEffectExecute:Damage → Health 的换算

这个方法在一次 Instant 类 GameplayEffect 执行完成后被调用,也是属性扣血的真正入口。

它是被谁调用的?

很多人读到这里会有个疑问:在整个 ActionRPG 工程里 grep PostGameplayEffectExecute,只找得到这份"实现",却找不到任何一处"调用",那它到底是被谁触发的?

答案是:调用它的代码不在本工程里,而在引擎的 GameplayAbilities 插件内部。 PostGameplayEffectExecute 是引擎基类 UAttributeSet 声明的一个 virtual 虚函数URPGAttributeSet 只是 override(重写) 了它------你只写实现,不写调用,调用是引擎回调过来的。这和 PreAttributeChangeOnRep_Health 完全是同一类机制。

真正的调用链在引擎侧(都位于 Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayEffect.cpp):

bash 复制代码
UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf
  └─ FActiveGameplayEffectsContainer::ExecuteActiveEffectsFrom   ← 执行 Instant GE
       ├─ 执行自定义 Execution
       │     └─ URPGDamageExecution::Execute_Implementation      ← 上一节
       │           (输出 = 往 Damage 属性加一个 OutputModifier)
       └─ 把输出的每一个 Modifier 逐个应用:
            └─ FActiveGameplayEffectsContainer::InternalExecuteMod
                 ├─ AttributeSet->PreGameplayEffectExecute(Data)
                 ├─ 实际写入 BaseValue
                 └─ AttributeSet->PostGameplayEffectExecute(Data)  ← ★就是在这里被调

关键在 InternalExecuteMod每往 BaseValue 写入一个 Modifier,引擎就会找到该属性所属的 AttributeSet,回调一次它的 PostGameplayEffectExecute 所以 URPGDamageExecutionDamage 属性 AddOutputModifier 出来的那"一个"增量,就是触发这次回调的直接原因。

换句话说,第二节的"输出加到 Damage 属性"和本节的"扣血"之间,其实还隔着一层引擎调用虚函数的动作------前面的全景图把这一层隐掉了,补上后应该是:

bash 复制代码
URPGDamageExecution::Execute_Implementation
  │  往 Damage 属性输出一个 OutputModifier
  ▼
【引擎】FActiveGameplayEffectsContainer::InternalExecuteMod
  │  写完 BaseValue 后,立刻调用 AttributeSet 的虚函数
  ▼
PostGameplayEffectExecute(URPGAttributeSet 重写)

想亲眼确认的话,打开 UE 4.27 安装目录下的 GameplayEffect.cpp,搜索 PostGameplayEffectExecute,即可在 InternalExecuteMod 里定位到那次调用。

用真实调用栈验证一遍(Debug 配置)

上面这条链是"读代码推断"出来的。有了源码引擎的 Debug 配置 (游戏+引擎全部未优化),我们可以直接在 PostGameplayEffectExecute第 102 行 (函数体第一条语句 Data.EffectSpec.GetContext())打断点,触发一次近战命中,把整条调用栈抓下来对照。

真实堆栈很长(几十帧),其中一大半是蓝图虚拟机执行字节码的"管道帧"------ProcessEventUFunction::InvokeFFrame::StepexecLetProcessLocalScriptFunction 等等。这些帧只说明"当前正在跑一段蓝图图表",本身没有业务含义,读栈时可以整段折叠掉。把管道帧去掉后,剩下的有业务含义的帧从下往上(最外层 → 断点)正好分成四段:

① 引擎 Tick + 物理重叠(最外层,伤害的物理起点)

bash 复制代码
UWorld::Tick
 └ UCharacterMovementComponent::TickComponent        ← 角色移动的 Tick
    └ PerformMovement → ~FScopedMovementUpdate        ← 移动更新收尾时结算重叠
       └ UpdateOverlaps → BeginComponentOverlap       ← 武器碰撞体扫过、与目标重叠
          └ AActor::NotifyActorBeginOverlap
             └ AActor::ReceiveActorBeginOverlap        ← 回调蓝图的 ActorBeginOverlap 事件

链路的真正起点不是"扣血",而是"这一帧角色动了" 。角色移动 Tick 里,武器碰撞体扫过目标,引擎在移动更新收尾(~FScopedMovementUpdate 析构)时结算重叠,检测到命中,于是回调蓝图的 ActorBeginOverlap

② 蓝图事件 → 发 GameplayEvent → 被 AbilityTask 接住

bash 复制代码
(蓝图虚拟机执行 Overlap 事件图表)
 └ SendGameplayEventToActor                            ← 蓝图把这次命中广播成一个 GameplayEvent
    └ UAbilitySystemComponent::HandleGameplayEvent
       └ ...Broadcast → URPGAbilityTask_PlayMontageAndWaitForEvent::OnGameplayEvent  ← 正在等事件的 AbilityTask 收到
          └ FRPGPlayMontageAndWaitForEventDelegate 广播回技能蓝图

这正对应前面提到的 RPGAbilityTask_PlayMontageAndWaitForEvent------技能一边播放攻击蒙太奇,一边等 EventReceived。命中产生的 GameplayEvent 被它接住,再广播回技能蓝图,驱动下一步。

③ 技能应用效果容器(EffectContainer → GE Spec)

bash 复制代码
URPGGameplayAbility::execApplyEffectContainer          ← 蓝图调用 ApplyEffectContainer
 └ ApplyEffectContainer → ApplyEffectContainerSpec      ← MakeSpec / ApplySpec 那一对
    └ K2_ApplyGameplayEffectSpecToTarget
       └ UGameplayAbility::ApplyGameplayEffectSpecToTarget
          └ UAbilitySystemComponent::ApplyGameplayEffectSpecToTarget
             └ ApplyGameplayEffectSpecToSelf            ← 施加到目标自己的 ASC 上

技能收到事件后,按 EffectContainerMap 取出对应的 GameplayEffect,组装成 Spec 并应用到目标身上。这就是第一节全景图里"MakeEffectContainerSpec → ApplyEffectContainerSpec"那一层的真身。

④ GE 执行 → 印证本节结论

bash 复制代码
UAbilitySystemComponent::ExecuteGameplayEffect              (行 934)
 └ FActiveGameplayEffectsContainer::ExecuteActiveEffectsFrom (行 2010)
    └ FActiveGameplayEffectsContainer::InternalExecuteMod    (行 2710)  ← 就是这里!
       └ URPGAttributeSet::PostGameplayEffectExecute         (行 102)  ← 断点命中

最里面这四层,和本节前面"读代码推断"出来的引擎调用链一字不差InternalExecuteMod 写完 BaseValue 后回调 PostGameplayEffectExecute。之前靠翻源码猜的路径,被真实堆栈实锤了。

把四段连起来,就得到一条比全景图更完整、更靠前的因果链:

bash 复制代码
角色移动 Tick → 武器碰撞体重叠 → 蓝图 ActorBeginOverlap
  → SendGameplayEventToActor(发 GameplayEvent)
  → PlayMontageAndWaitForEvent 接住事件
  → 技能 ApplyEffectContainer(取 GE、组 Spec、应用)
  → ExecuteGameplayEffect → InternalExecuteMod
  → PostGameplayEffectExecute → 扣血

断点此刻停在这根栈的最顶端 ,往下看每一帧,就能亲眼看到"到底是谁、经过哪些环节调用了我"。这也顺带说明了为什么值得费劲编译一套 Debug 引擎:只有游戏和引擎都未优化,这根跨越"引擎 Tick → GAS 插件 → 你自己的 AttributeSet"的完整栈才不会被优化折叠,每一帧的参数也才看得清。

为什么偏偏是它触发,而不是别的回调?

GE 按 Duration Policy(持续策略)分三种:Instant(瞬时)、Has Duration(限时)、Infinite(永久)。后两种改的是 CurrentValue(临时叠加,到期自动还原),走的是 PreAttributeChange;只有 Instant GE 直接改 BaseValue (永久改变),改完后才触发 PostGameplayEffectExecute

ActionRPG 的伤害 GE 正是 Instant 类型------扣血是一次性的永久修改,不存在"10 秒后血量自动恢复"这种语义,所以走的是"改 BaseValue → 触发 PostGameplayEffectExecute"这条路。这也顺带解释了:为什么扣血逻辑要写在这个回调里,而不是写在 PreAttributeChange 里。

理解了"谁调用、何时调用"之后,再看实现。上一篇已经介绍过它的整体结构,这里专注伤害分支:

cpp 复制代码
void URPGAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
    Super::PostGameplayEffectExecute(Data);

    // ... 解析 Context、获取 Source/Target Actor ...

    if (Data.EvaluatedData.Attribute == GetDamageAttribute())
    {
        // 1. 本地复制伤害值,并立刻清零(Damage 是临时中转站)
        const float LocalDamageDone = GetDamage();
        SetDamage(0.f);

        if (LocalDamageDone > 0)
        {
            // 2. 扣血并钳制到 [0, MaxHealth]
            const float OldHealth = GetHealth();
            SetHealth(FMath::Clamp(OldHealth - LocalDamageDone, 0.0f, GetMaxHealth()));

            if (TargetCharacter)
            {
                // 3. 触发角色的伤害回调
                TargetCharacter->HandleDamage(LocalDamageDone, HitResult,
                    SourceTags, SourceCharacter, SourceActor);
                // 4. 触发血量变化回调
                TargetCharacter->HandleHealthChanged(-LocalDamageDone, SourceTags);
            }
        }
    }
    else if (Data.EvaluatedData.Attribute == GetHealthAttribute())
    {
        // 治疗/直接 Health 修改的分支
        SetHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth()));
        if (TargetCharacter)
            TargetCharacter->HandleHealthChanged(DeltaValue, SourceTags);
    }
    // ... Mana、MoveSpeed 分支同理 ...
}

这段代码的逻辑可以分四步理解:

第一步:取出 Damage 并立即清零

GetDamage() 读出 URPGDamageExecution 写进来的伤害值,SetDamage(0.f) 马上清零。这正是属性系统精读里讲的"Damage 是临时中转站"------它的生命周期就在这几行代码之间,用完即清。

第二步:扣血并钳制(Clamp)

"钳制"是 FMath::Clamp 的中文叫法,意思是把一个数值强行限制在给定的上下限区间内:小于下限就取下限,大于上限就取上限,落在中间才用原值。

FMath::Clamp(OldHealth - LocalDamageDone, 0.0f, GetMaxHealth()) 就是把"扣血后的新血量"限制到 [0, MaxHealth]:算出来若是负数(被打死了),取 0,血量不会变成负值;若超过了最大血量(比如同时还在回血),取 MaxHealth,不会溢出上限。夹好之后再调 SetHealth------这走的是 ASC 的正规通道,SetHealth 内部通过 ApplyModToAttributeUnsafe 修改 CurrentValue

第三步:调 HandleDamage

这是一个"转发层"。HandleDamage 不做额外计算,直接调 OnDamaged(BlueprintImplementableEvent):

cpp 复制代码
void ARPGCharacterBase::HandleDamage(float DamageAmount, const FHitResult& HitInfo,
    const FGameplayTagContainer& DamageTags,
    ARPGCharacterBase* InstigatorPawn, AActor* DamageCauser)
{
    OnDamaged(DamageAmount, HitInfo, DamageTags, InstigatorPawn, DamageCauser);
}

为什么要有这个 C++ 中间层?因为 C++ 里的子类(比如 NPC 基类)可以重写 HandleDamage 做额外逻辑(记录伤害统计、触发 AI 行为树等),而蓝图的 OnDamaged 保持干净。这是"C++ 提供 Hook,蓝图实现内容"模式的典型体现。

第四步:调 HandleHealthChanged

注意参数是 -LocalDamageDone(负数),表示血量减少 了。治疗分支传的是 DeltaValue(正数)。接收这个回调的蓝图可以统一处理"血量变化显示":正数刷新治疗特效,负数刷新受伤血量跳字。


四、HandleDamage → OnDamaged 回调的设计

ARPGCharacterBase 里有四个 BlueprintImplementableEvent

cpp 复制代码
UFUNCTION(BlueprintImplementableEvent)
void OnDamaged(float DamageAmount, const FHitResult& HitInfo,
    const FGameplayTagContainer& DamageTags,
    ARPGCharacterBase* InstigatorCharacter, AActor* DamageCauser);

UFUNCTION(BlueprintImplementableEvent)
void OnHealthChanged(float DeltaValue, const FGameplayTagContainer& EventTags);

UFUNCTION(BlueprintImplementableEvent)
void OnManaChanged(float DeltaValue, const FGameplayTagContainer& EventTags);

UFUNCTION(BlueprintImplementableEvent)
void OnMoveSpeedChanged(float DeltaValue, const FGameplayTagContainer& EventTags);

它们各自对应 PostGameplayEffectExecute 里的四个 else if 分支。C++ 是事件的"发射端",蓝图是事件的"实现端"。

OnDamagedOnHealthChanged 在一次伤害里会同时被调:

  • OnDamaged:带完整上下文(攻击者、HitResult、伤害标签)------适合实现受击动画、伤害来源判定、死亡逻辑。
  • OnHealthChanged:更通用,只有"变化量和原因标签"------适合更新血条 UI、显示飘字伤害数。

为什么分开两个?因为治疗不触发 OnDamaged,但会触发 OnHealthChanged。血条 UI 绑定 OnHealthChanged,无论是受伤还是治疗都自动更新,而受击反馈只绑 OnDamaged。这样各自独立,互不干扰。


五、Source 的解析与 GetEffectCauser

PostGameplayEffectExecute 里有一段"解析攻击者"的代码,看起来有点啰嗦,但每一步都有道理:

cpp 复制代码
AActor* SourceActor = nullptr;
AController* SourceController = nullptr;
ARPGCharacterBase* SourceCharacter = nullptr;

if (Source && Source->AbilityActorInfo.IsValid()
    && Source->AbilityActorInfo->AvatarActor.IsValid())
{
    SourceActor = Source->AbilityActorInfo->AvatarActor.Get();
    SourceController = Source->AbilityActorInfo->PlayerController.Get();

    // PlayerController 为空时,尝试从 Pawn 拿
    if (SourceController == nullptr && SourceActor != nullptr)
    {
        if (APawn* Pawn = Cast<APawn>(SourceActor))
            SourceController = Pawn->GetController();
    }

    if (SourceController)
        SourceCharacter = Cast<ARPGCharacterBase>(SourceController->GetPawn());
    else
        SourceCharacter = Cast<ARPGCharacterBase>(SourceActor);

    // 如果 Context 里记录了 EffectCauser,覆盖 SourceActor
    if (Context.GetEffectCauser())
        SourceActor = Context.GetEffectCauser();
}

最后那行 GetEffectCauser() 覆盖是关键。这里要分清两个容易混的概念:

  • Instigator(发起者):真正"发动这次攻击的人"------玩家或角色本体,攻击者的 ASC 就挂在它身上。
  • EffectCauser(直接致害物) :真正"在物理上碰到目标、造成这次效果的 Actor"。近战里往往是挥出去的那把 ,远程里是飞出去的那颗火球,而不是玩家本体。

某些场景下这两者不是同一个东西:攻击的 ASC 属于玩家(发起者),但直接碰到目标的是武器(致害物)。Context.GetEffectCauser() 记录的就是这个"直接致害物"(那把剑)。OnDamagedDamageCauser 参数拿到的正是它------蓝图可以据此播放对应的武器命中特效(比如剑砍中时的火花),而不用去管背后是哪个玩家发动的。


六、小结:设计亮点

把全管线梳理一遍,几个值得记忆的设计点:

位置 设计 原因
RPGDamageStatics AttackPower 快照,DefensePower 不快照 攻击力锁发射时,防御力用命中时
Execute_Implementation 输出加到 Damage 属性,而不是直接扣血 让 AttributeSet 统一处理后处理逻辑
PostGameplayEffectExecute 立刻 SetDamage(0) 清零 Damage 是中转站,不是持久状态
HandleDamage C++ 中间层转发到 OnDamaged 允许 C++ 子类重写,同时蓝图干净
OnDamaged + OnHealthChanged 分开 受击逻辑与血量 UI 解耦 治疗只触发后者,不触发前者

整条管线理解之后,再看 URPGDamageExecution 里那句注释------"Most games will want to implement multiple game-specific executions"------就能真切体会到它的意思:ActionRPG 的 Damage × AttackPower / DefensePower 只是个示例公式,真实游戏里你可以写元素克制、护盾吸收、暴击乘数......统统塞进 Execute_Implementation,而 PostGameplayEffectExecute 那一层完全不用动。这就是 ExecutionCalculation 抽象的价值所在。

相关推荐
郝学胜-神的一滴2 天前
[简化版 GAMES 101] 计算机图形学 16:纹理走样、Mipmap、三线性插值、各向异性过滤原理全解
unity·游戏引擎·godot·图形渲染·three.js·opengl·unreal
夜猫逐梦21 天前
【UE基础】03.蓝图与编辑器工作流
编辑器·ue·蓝图·ue编辑器
夜猫逐梦22 天前
【UE基础】02.基类与蓝图子类的分工
ue·蓝图·ue基础
夜猫逐梦22 天前
【UE基础】01.环境与引擎心智模型
游戏·逆向·ue·unreal·actionrpg
郝学胜-神的一滴23 天前
[简化版 GAMES 101] 计算机图形学 13:从光栅化到着色——赋予三维像素光影灵魂
c++·计算机视觉·unity·godot·图形渲染·opengl·unreal
数峦云数字孪生三维可视化25 天前
数字孪生可视化架构选型:基于UE像素流送与Web引擎的完整决策指南
数字孪生·ue·web3d·技术选型·像素推流
郝学胜-神的一滴1 个月前
中级OpenGL教程 009:用环境光告别模型死黑
前端·c++·unity·godot·图形渲染·opengl·unreal
挨代码1 个月前
UEC++ 数据类型及继承关系
c++·ue
郝学胜-神的一滴1 个月前
[简化版 GAMES 101] 计算机图形学 12:可见性与 Z‑Buffer 深度缓存
unity·godot·图形渲染·three.js·opengl·unreal