UE5物理系统实战:从零开始用C++实现物体碰撞与射线检测(附完整代码)

UE5物理系统实战:从零开始用C++实现物体碰撞与射线检测(附完整代码)

如果你已经对UE5的蓝图系统驾轻就熟,但总觉得在实现一些复杂的物理交互时,蓝图节点显得力不从心,或者性能上遇到了瓶颈,那么是时候深入C++层面了。物理系统是游戏真实感和交互性的基石,从角色被墙壁阻挡,到拾取物品时触发的音效,再到解谜游戏中精准的射线探测,背后都是物理引擎在默默工作。本文将带你绕过表面的蓝图封装,直接使用C++与UE5的物理引擎对话。我们将从创建一个受重力影响的物理对象开始,逐步实现精确的碰撞事件响应,并最终完成一个高效的射线检测系统。整个过程将伴随着可直接集成到你项目中的完整代码,目标是让你不仅知其然,更能知其所以然,从而在未来的开发中能灵活定制属于自己的物理规则。

1. 构建你的第一个C++物理Actor

在UE5中,一切可放置的对象几乎都继承自AActor。要让一个Actor参与物理模拟,关键在于为其附加一个启用了物理模拟的原始组件(Primitive Component),最常见的就是UStaticMeshComponent。让我们从零开始构建这个基础。

1.1 创建C++类与组件初始化

首先,在编辑器内通过“工具”->“新建C++类”创建一个继承自Actor的类,命名为PhysicsCubeActor。创建完成后,打开对应的头文件(.h)和源文件(.cpp)。

在头文件中,我们需要声明一个静态网格体组件,并将其设置为根组件。同时,为了后续方便调试和配置,我们通过UPROPERTY宏将其暴露给编辑器。

// PhysicsCubeActor.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PhysicsCubeActor.generated.h"

UCLASS()
class YOURPROJECT_API APhysicsCubeActor : public AActor
{
    GENERATED_BODY()

public:
    // 设置默认属性
    APhysicsCubeActor();

protected:
    virtual void BeginPlay() override;

private:
    // 声明静态网格体组件
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
    class UStaticMeshComponent* StaticMeshComp;
};

接下来,在源文件中实现构造函数。这里有几个关键步骤:

  1. 创建组件:使用CreateDefaultSubobject函数创建组件实例。这是UE反射系统管理UObject的标准方式。
  2. 设置根组件:将创建的网格体组件设为根组件(Root Component),这决定了Actor在世界中的变换原点。
  3. 加载默认资产:为了方便,我们可以直接在构造函数中加载一个引擎内置的基本形状(如立方体)作为网格体。
  4. 启用物理:这是核心一步,调用SetSimulatePhysics(true)来告知物理引擎接管该组件的运动。
// PhysicsCubeActor.cpp
#include "PhysicsCubeActor.h"
#include "Components/StaticMeshComponent.h"
#include "UObject/ConstructorHelpers.h"

APhysicsCubeActor::APhysicsCubeActor()
{
    // 允许每帧调用Tick函数
    PrimaryActorTick.bCanEverTick = true;

    // 创建并设置静态网格体组件
    StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshComponent"));
    RootComponent = StaticMeshComp;

    // 从引擎内容中查找并加载一个基本的立方体网格体
    static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Engine/BasicShapes/Cube.Cube"));
    if (MeshAsset.Succeeded())
    {
        StaticMeshComp->SetStaticMesh(MeshAsset.Object);
    }

    // 启用物理模拟
    StaticMeshComp->SetSimulatePhysics(true);
    // 可选:设置质量(单位:千克)
    StaticMeshComp->SetMassOverrideInKg(NAME_None, 50.0f, true);
}

void APhysicsCubeActor::BeginPlay()
{
    Super::BeginPlay();
}

编译项目后,你可以在内容浏览器中找到这个APhysicsCubeActor类,将其拖入场景。运行游戏,你会看到这个立方体在重力作用下坠落。恭喜,你已经用C++创建了第一个物理对象!

注意:在构造函数中加载资产(如ConstructorHelpers::FObjectFinder)仅适用于设置默认值。对于需要在运行时动态加载或更改的资产,应在BeginPlay或其他游戏逻辑函数中使用异步加载器。

1.2 物理属性的深度配置

仅仅启用物理模拟只是开始。为了满足不同的游戏需求,我们经常需要精细地调整物理属性。这些属性大多可以通过UStaticMeshComponent或其底层的BodyInstance进行访问和设置。

以下是一些常用且关键的物理属性配置示例,我们可以在构造函数或一个自定义的初始化函数中设置它们:

// 在构造函数中继续配置物理属性
APhysicsCubeActor::APhysicsCubeActor()
{
    // ... 之前的创建和加载代码 ...

    // 1. 设置线性阻尼(空气阻力)和角阻尼(旋转阻力)
    StaticMeshComp->SetLinearDamping(0.01f);
    StaticMeshComp->SetAngularDamping(0.05f);

    // 2. 约束某些轴上的移动或旋转(例如,制作一个只能在XZ平面滑动的物体)
    StaticMeshComp->BodyInstance.bLockXRotation = true;
    StaticMeshComp->BodyInstance.bLockYRotation = true;
    StaticMeshComp->BodyInstance.bLockZRotation = false; // 允许绕Z轴旋转
    // StaticMeshComp->BodyInstance.bLockTranslation = true; // 完全锁定移动

    // 3. 设置碰撞响应通道。这里先简单设置,后续章节会详细展开。
    // StaticMeshComp->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);

    // 4. 启用重力(默认就是true,这里演示如何关闭)
    // StaticMeshComp->SetEnableGravity(false);

    // 5. 设置物理材质(影响摩擦力、弹性等)
    // 需要先创建或引用一个PhysicalMaterial资产
    // UPhysicalMaterial* MyPhysMat = ...;
    // StaticMeshComp->SetPhysMaterialOverride(MyPhysMat);
}

通过组合这些属性,你可以创造出行为各异的物理实体,比如漂浮的幽灵物体、急速旋转的陀螺,或者像冰面一样光滑的移动平台。

2. 碰撞检测:Overlap与Block的实战处理

在UE5的物理世界中,碰撞检测主要分为两种类型:Overlap(重叠)Block(阻挡)。理解它们的区别是设计正确交互逻辑的前提。

检测类型物理响应典型应用场景性能开销
Overlap物体相互穿透,不产生推力。触发器区域、拾取物品、伤害区域、检测玩家进入范围。较低,仅进行体积测试。
Block物体相互阻挡,物理引擎会计算反作用力,阻止穿透。墙壁、地面、可移动的箱子、角色之间的碰撞。较高,需进行复杂的约束求解。

2.1 绑定与处理Overlap事件

假设我们要让PhysicsCubeActor在与其他Actor(比如玩家角色)重叠时,在屏幕上打印一条日志。我们需要绑定OnComponentBeginOverlap委托。

首先,在头文件中声明委托回调函数:

// PhysicsCubeActor.h
// ... 在类声明内部添加 ...
private:
    // 重叠事件委托函数
    UFUNCTION()
    void OnCompBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

然后,在BeginPlay函数中,将这个函数绑定到我们网格体组件的OnComponentBeginOverlap委托上:

// PhysicsCubeActor.cpp
void APhysicsCubeActor::BeginPlay()
{
    Super::BeginPlay();

    // 确保组件有效后绑定事件
    if (StaticMeshComp)
    {
        StaticMeshComp->OnComponentBeginOverlap.AddDynamic(this, &APhysicsCubeActor::OnCompBeginOverlap);
    }
}

// 实现重叠事件处理函数
void APhysicsCubeActor::OnCompBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    // 简单的有效性检查,避免自己与自己重叠的误报
    if (OtherActor && (OtherActor != this) && OtherComp)
    {
        FString Message = FString::Printf(TEXT("[Overlap] %s 与 %s 发生重叠"), *GetName(), *OtherActor->GetName());
        GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, Message);
        UE_LOG(LogTemp, Warning, TEXT("%s"), *Message);
    }
}

现在运行游戏,控制角色走到立方体旁边(确保角色胶囊体与立方体网格体重叠),你就能在屏幕左上角和输出日志中看到重叠信息。

提示:OnComponentBeginOverlap是组件级别的事件,比OnActorBeginOverlap(Actor级别)更常用,因为它能提供更具体的碰撞组件信息(OtherComp),这对于处理复杂Actor(由多个碰撞体组成)的交互至关重要。

2.2 实现Blocking碰撞与力反馈

Blocking碰撞通常不需要我们手动绑定事件,因为物理引擎会自动处理阻挡和力的计算。但我们可以通过它来施加一些自定义的力或实现特殊效果。例如,让我们的立方体在被角色撞击时,朝反方向弹开。

我们可以在Tick函数中检查速度变化,或者更直接地,通过计算碰撞后的冲量来施加一个力。这里演示一个更主动的方法:在角色控制器中,当按下某个键时,向面前的立方体施加一个冲力。

首先,在玩家角色或控制器类中实现一个函数:

// 在玩家角色类中
void AMyCharacter::ApplyImpulseToPhysicsActor()
{
    // 执行一次射线检测,找到面前的Actor(射线检测实现见下一章)
    FHitResult Hit;
    FVector Start = GetActorLocation();
    FVector End = Start + GetActorForwardVector() * 500.0f;
    FCollisionQueryParams Params;
    Params.AddIgnoredActor(this);

    if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
    {
        APhysicsCubeActor* HitCube = Cast<APhysicsCubeActor>(Hit.GetActor());
        if (HitCube && HitCube->StaticMeshComp)
        {
            // 获取击中点的法线方向
            FVector ImpulseDir = Hit.ImpactNormal;
            // 施加一个沿着法线方向的冲力(例如,击退)
            float ImpulseStrength = 1000.0f;
            HitCube->StaticMeshComp->AddImpulse(ImpulseDir * ImpulseStrength, NAME_None, true);

            // 也可以施加一个径向力,产生爆炸效果
            // HitCube->StaticMeshComp->AddRadialImpulse(Hit.Location, 300.0f, 5000.0f, ERadialImpulseFalloff::RIF_Constant, true);
        }
    }
}

将这个函数绑定到某个输入按键(如“F”键),你就能在游戏中“击飞”物理立方体了。这种直接对物理组件施加力的方式,是实现互动玩法的强大工具。

3. 射线检测:从屏幕到世界的精准感知

射线检测(Raycast)是游戏开发中用途最广的技术之一,常用于武器瞄准、物体拾取、视线判断、地形探测等。其原理是从世界空间中的一个起点,沿着一个方向发射一条无形的线段,并检测沿途与哪些物体相交。

3.1 实现基础的单点射线检测

我们将在一个玩家角色类中实现一个简单的射线检测功能:从摄像机位置向前方发射射线,检测击中的物体并高亮显示。

在角色头文件中声明函数和变量:

// MyCharacter.h
// ... 类声明内 ...
public:
    // 执行射线检测的函数
    void PerformRaycast();

protected:
    // 摄像机组件引用,用于获取射线起点和方向
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
    class UCameraComponent* FirstPersonCamera;

    // 用于存储上次击中的Actor,以便取消高亮
    AActor* LastHitActor;
    // 高亮材质实例
    UMaterialInstanceDynamic* HighlightMaterial;

在角色源文件中实现:

// MyCharacter.cpp
#include "MyCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/World.h"
#include "DrawDebugHelpers.h" // 用于调试绘制

void AMyCharacter::PerformRaycast()
{
    if (!FirstPersonCamera) return;

    FVector Start = FirstPersonCamera->GetComponentLocation();
    // 射线终点:起点 + 摄像机前方向量 * 检测距离
    FVector End = Start + (FirstPersonCamera->GetForwardVector() * 10000.0f);

    FHitResult HitResult;
    FCollisionQueryParams QueryParams;
    QueryParams.bTraceComplex = true; // 使用复杂碰撞(基于三角形),更精确但更耗性能
    QueryParams.bReturnPhysicalMaterial = true; // 如果需要获取物理材质信息
    QueryParams.AddIgnoredActor(this); // 忽略自身,避免射线打到自己

    bool bHit = GetWorld()->LineTraceSingleByChannel(
        HitResult,
        Start,
        End,
        ECC_Visibility, // 使用“可见性”碰撞通道,这是最常用的之一
        QueryParams
    );

    // 如果上次有高亮对象,先恢复其材质
    if (LastHitActor)
    {
        APhysicsCubeActor* LastCube = Cast<APhysicsCubeActor>(LastHitActor);
        if (LastCube && LastCube->StaticMeshComp)
        {
            // 这里需要你事先保存原始材质,这里简化为移除动态材质
            // LastCube->StaticMeshComp->SetMaterial(0, OriginalMaterial);
        }
        LastHitActor = nullptr;
    }

    if (bHit)
    {
        AActor* HitActor = HitResult.GetActor();
        if (HitActor)
        {
            // 在屏幕上显示击中信息
            GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Red,
                FString::Printf(TEXT("击中: %s (距离: %.2f)"), *HitActor->GetName(), HitResult.Distance));

            // 尝试高亮击中的物理立方体
            APhysicsCubeActor* HitCube = Cast<APhysicsCubeActor>(HitActor);
            if (HitCube && HitCube->StaticMeshComp)
            {
                // 创建或设置一个高亮材质
                if (!HighlightMaterial)
                {
                    // 这里需要你有一个基础的高亮材质(如M_Highlight)
                    UMaterialInterface* BaseMat = LoadObject<UMaterialInterface>(nullptr, TEXT("/Game/Materials/M_Highlight.M_Highlight"));
                    if (BaseMat)
                    {
                        HighlightMaterial = UMaterialInstanceDynamic::Create(BaseMat, this);
                        HighlightMaterial->SetVectorParameterValue(TEXT("Color"), FLinearColor::Red);
                    }
                }
                if (HighlightMaterial)
                {
                    // 保存原始材质(实际项目中应更妥善地管理)
                    // OriginalMaterial = HitCube->StaticMeshComp->GetMaterial(0);
                    HitCube->StaticMeshComp->SetMaterial(0, HighlightMaterial);
                    LastHitActor = HitActor;
                }
            }

            // 调试:在编辑器中绘制击中点和法线
            #if WITH_EDITOR
            DrawDebugPoint(GetWorld(), HitResult.ImpactPoint, 10.0f, FColor::Green, false, 1.0f);
            DrawDebugLine(GetWorld(), HitResult.ImpactPoint, HitResult.ImpactPoint + HitResult.ImpactNormal * 50.0f, FColor::Blue, false, 1.0f);
            #endif
        }
    }
    else
    {
        // 调试:绘制完整的射线(未击中时)
        #if WITH_EDITOR
        DrawDebugLine(GetWorld(), Start, End, FColor::White, false, 0.5f);
        #endif
    }
}

最后,在角色输入绑定中(如SetupPlayerInputComponent函数),将PerformRaycast函数绑定到鼠标左键或某个按键上。

3.2 高级射线检测:形状扫描与多目标检测

除了LineTraceSingleByChannel(单射线单目标),UE5还提供了其他强大的检测函数:

  • LineTraceMultiByChannel:检测射线路径上的所有物体,返回一个TArray<FHitResult>。适用于霰弹枪、激光穿透等效果。
    TArray<FHitResult> HitResults;
    if (GetWorld()->LineTraceMultiByChannel(HitResults, Start, End, ECC_Visibility, QueryParams))
    {
        for (const FHitResult& Hit : HitResults)
        {
            // 处理每一个被击中的物体
        }
    }
    
  • SweepSingleByChannel:使用一个碰撞形状(如球体、胶囊体、盒子)沿着路径进行扫描检测。这比射线更符合某些体积性武器的检测逻辑(如挥舞刀剑、爆炸冲击波)。
    FHitResult SweepHit;
    FCollisionShape SphereShape = FCollisionShape::MakeSphere(50.0f); // 创建一个半径为50的球体
    bool bSweepHit = GetWorld()->SweepSingleByChannel(
        SweepHit,
        Start,
        End,
        FQuat::Identity, // 旋转
        ECC_WorldDynamic, // 碰撞通道
        SphereShape,
        QueryParams
    );
    

选择哪种检测方式,取决于具体的游戏需求。单射线检测效率最高,多目标检测和形状扫描则能实现更复杂的交互逻辑,但性能开销也相应增大。

4. 碰撞通道与响应:构建精细的交互规则

当游戏中的物体种类增多时,让所有物体都相互阻挡或重叠会产生混乱。UE5的**碰撞通道(Collision Channel)响应(Response)**系统就是为了解决这个问题而设计的。它允许你定义物体属于哪些通道,以及它们如何响应来自其他通道的碰撞查询。

4.1 理解预设与自定义通道

在项目设置(Project Settings -> Collision)中,你可以看到一系列预设的碰撞通道,如WorldStaticWorldDynamicPawnPhysicsBody等。每个UPrimitiveComponent都有一个Collision Responses属性,决定了它对这些预设通道的响应(Ignore, Overlap, Block)。

然而,对于复杂项目,我们经常需要创建自定义通道。例如,为“子弹”、“敌人”、“可破坏物”分别创建通道。

  1. 创建自定义对象通道:在项目设置的“Collision”->“Object Channels”中,点击“New Object Channel...”,命名为“Bullet”,默认响应设为“Ignore”。
  2. 创建自定义追踪通道:在“Trace Channels”中,可以创建用于射线检测的通道,例如“SightLine”。

4.2 在C++中配置碰撞响应

创建好通道后,我们可以在C++代码中动态地设置组件的碰撞响应。以下代码演示了如何让我们的PhysicsCubeActor只阻挡“WorldStatic”通道,与“Pawn”通道重叠,并忽略“Bullet”通道(假设子弹有自己的处理逻辑)。

// 在APhysicsCubeActor的构造函数或初始化函数中
APhysicsCubeActor::APhysicsCubeActor()
{
    // ... 之前的代码 ...

    if (StaticMeshComp)
    {
        // 1. 首先,获取组件当前的碰撞响应容器
        FCollisionResponseContainer& ResponseContainer = StaticMeshComp->GetCollisionResponseToChannels();

        // 2. 设置对特定预设通道的响应
        ResponseContainer.SetResponse(ECC_WorldStatic, ECR_Block);   // 阻挡静态世界物体
        ResponseContainer.SetResponse(ECC_WorldDynamic, ECR_Overlap); // 与动态物体重叠
        ResponseContainer.SetResponse(ECC_Pawn, ECR_Overlap);        // 与Pawn重叠
        ResponseContainer.SetResponse(ECC_PhysicsBody, ECR_Ignore);  // 忽略其他物理体(避免堆叠时过度推挤)

        // 3. 设置对自定义通道的响应(需要先获取通道ID)
        // 假设我们通过项目设置创建了“Bullet”对象通道
        // ECollisionChannel BulletChannel = ECC_GameTraceChannel1; // 这需要根据实际分配确定
        // ResponseContainer.SetResponse(BulletChannel, ECR_Ignore);

        // 4. 将修改后的响应设置回组件
        // 注意:更推荐在组件创建后,通过蓝图或细节面板预设好Collision Preset,代码中仅做微调。
        // StaticMeshComp->SetCollisionResponseToChannels(ResponseContainer);

        // 更简洁的方式:直接使用SetCollisionResponseToChannel函数
        StaticMeshComp->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
        StaticMeshComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
        // ... 其他通道设置
    }
}

通过精细的碰撞通道配置,你可以实现诸如“玩家可以穿过队友但会被敌人阻挡”、“子弹只击中敌人和可破坏物”、“魔法效果只影响特定阵营”等复杂的游戏规则,而无需编写冗长的条件判断代码。这套系统是UE5物理交互能够保持清晰和高效的核心所在。

5. 性能优化与调试技巧

当场景中物理对象成百上千,或者每帧进行大量射线检测时,性能问题就会凸显。这里分享几个在C++层面优化物理性能的实战经验。

首先,善用碰撞预设(Collision Presets)而非全动态设置。 在编辑器中为常用的碰撞类型(如“PhysicsActor”、“TriggerVolume”、“NoCollision”)创建预设,然后在C++中通过StaticMeshComp->SetCollisionProfileName(TEXT("PhysicsActor"))一键应用。这比在运行时逐个设置通道要高效得多,也便于设计师调整。

其次,区分复杂碰撞(Complex Collision)和简单碰撞(Simple Collision)。 默认情况下,静态网格体使用其自带的简化碰撞体(如包围盒、凸包)。bTraceComplex参数为false时,射线检测就使用这个简化体,速度极快。只有需要极高精度(如子弹击中装甲缝隙)时才开启复杂碰撞(基于三角形网格)。对于大量的小型检测,务必使用简单碰撞。

FCollisionQueryParams Params;
Params.bTraceComplex = false; // 默认就是false,确保你不小心改了回来
// 或者针对特定查询关闭复杂碰撞
Params.bTraceComplex = false;

第三,减少不必要的每帧检测。 不是所有射线检测都需要在Tick中执行。例如,拾取物品的检测可以在玩家按下按键时触发;敌人的视线检测可以用一个定时器(FTimerHandle)每0.1-0.2秒执行一次。

// 在BeginPlay中设置一个定时器
GetWorld()->GetTimerManager().SetTimer(SightCheckTimerHandle, this, &AEnemyAI::CheckSightLine, 0.15f, true);

最后,充分利用调试工具。 UE5编辑器提供了强大的物理调试可视化功能。在运行时控制台(按“~”键)输入命令可以显示碰撞体、物理网络、接触点等信息,这对于排查诡异的物理行为至关重要。

// 显示所有碰撞体的形状
show collision

// 显示物理模拟的刚体
show rigidbody

// 显示射线检测的调试信息(需要在代码中启用DrawDebug)
show debugweapon

我在一个拥有大量可互动物理道具的项目中,最初因为所有道具都启用了复杂碰撞且每帧进行Overlap检测,导致了明显的帧率下降。后来通过将大部分道具的碰撞预设改为仅对“Pawn”通道Overlap,并将持续检测改为由玩家靠近时触发的离散检测,性能提升了超过30%。物理系统的优化往往在于“做减法”,明确每个物体“需要”与谁交互,而不是“可以”与谁交互。

「LLM那些事」系列第 4 篇《上下文窗口的边界》,文章连接:https://blog.csdn.net/houwenjin/article/details/163999753。 演示什么:在「预测」Sheet 的黄色格子里输入一句话(默认「来泡一杯」),四个「模型」——分别只统计最后 1 / 2 / 3 / 4 个字的 n-gram 查表——同时预测下一个字。同一个输入,看的上下文越长,候选越少、预测越确定: ┌────────────────┬──────────┬───────────────┬──────┐ │ 只看最后几个字 │ 用的前缀 │ 候选下一字数 │ 预测 │ ├────────────────┼──────────┼───────────────┼──────┤ │ 1 个 │ 杯 │ 3(茶/子/水) │ 模糊 │ ├────────────────┼──────────┼───────────────┼──────┤ │ 2 个 │ 一杯 │ 2(茶/水) │ 收窄 │ ├────────────────┼──────────┼───────────────┼──────┤ │ 3 个 │ 泡一杯 │ 1(茶) │ 确定 │ ├────────────────┼──────────┼───────────────┼──────┤ │ 4 个 │ 来泡一杯 │ 1(茶) │ 确定 │ └────────────────┴──────────┴───────────────┴──────┘
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值