2014-07-25 17:49:38 u010153703 阅读数 1549
  • 从这里开始虚幻4-第2辑-蓝图 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2158 人正在学习 去看看 杨石兴
1. 在Personal里,往角色的SkeletonTree中合适的位置Add Socket。如果碰撞体要绑定到已经存在的Bone上,请略过此步骤。
2. CharacterBP的Component里,按照希望的形状,添加BoxComponent\SphereComponent\CapusuleComponent等作为碰撞体,并注意调整它们的大小,也尽量调整到合适的位置。
3. 在CharacterBP的Graph中的Construction Script里,将步骤2中添加的碰撞体绑定到希望的Socket上。例如:

AttachType有三种选择,一般选择Snap to Target(碰撞体和绑定的Socket无缝隙在一起),或者Keep Relative offset(碰撞体和所绑定Socket保持一段距离)。

碰撞体和物理是两回事。不过却经常和物理相结合来使用。可能需要在合适的时机给碰撞体开启/关闭物理。



2019-08-01 21:09:17 zyf918 阅读数 54
  • 从这里开始虚幻4-第2辑-蓝图 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2158 人正在学习 去看看 杨石兴

Unreal Engine——碰撞系统

在游戏的开发之中,玩家和场景的交互,有一点就是碰撞,这里讲解最基础的碰撞

一、碰撞的删除与添加

游戏会使用到许多的模型,有的时候,有的模型不想和玩家碰撞就要取消,例如:草。(UE4的植被系统可以自动解决)如果草的模型不取消碰撞,会出现下面这种人物站立在草上方的情况在这里插入图片描述
我们只需要打开草的这个模型
在这里插入图片描述
在Collision中选择移除碰撞在这里插入图片描述
添加碰撞,按照给出的碰撞模式添加即可
编译保存后,再进入就不会再有碰撞产生
在这里插入图片描述

2016-08-31 10:50:43 mrma55555 阅读数 5313
  • 从这里开始虚幻4-第2辑-蓝图 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2158 人正在学习 去看看 杨石兴

1、碰撞组件基本属性

碰撞组件的属性如图

这里写图片描述

  • 属性简要介绍:
  • Simulation Generates Hit Events:是否对碰撞事件进行通知
  • Phys Material Override:指定该Mesh的物理材质
  • Generate Overlap Events:是否对重叠事件进行通知
  • Collision Presets:碰撞预设
    • Collision Enabled:
      • No Collision:无碰撞
      • Query Only(No Physics Collision):仅响应踪迹碰撞,无物理碰撞
      • Physics Only(No Query Collision):仅响应物理碰撞,无踪迹碰撞
      • Collision Enabled(Physics and Query):同时响应物理碰撞和踪迹碰撞
  • Object Type:表示当前Mesh的对象类型(用于区别场景的Mesh类型,以便进行不同的碰撞响应)
  • Collision Responses:碰撞响应设置
    • Trace Responses:踪迹响应
      • Trace Channel Setting:踪迹响应的相关设置,通常用于射线的碰撞检测。假若现在有一束对Visibility通道进行检测的射线穿过该物体时,如该Mesh的Visibility选择为Block,将会触发碰撞事件,如该Mesh的Visibility选择为Ignore,那么将不会触发碰撞事件。
    • Object Responses: 对象响应
      • Object Type Setting:对象相应的相关设置,可用于射线检测,不同Mesh之间的物理交互等。假若现在有两个Cube(Object Type均为WorldStatic)迎面相撞,那么当Cube1和Cube2的对象响应下的WorldStatic均设置为Block时,两个Cube会相互阻挡对方。否则将会互相穿过对方。
  • Use CCD:是否针对该对象应用连续碰撞检测,增加检测的准确度。
  • Always Create Plysics State:是否总是创建物理状态(它的碰撞属性、质量、休眠等)。设置为真时可减少计算对象的物理状态的性能消耗来提高游戏性能。
  • Multi Body Overlap:如果此值为true,如重叠的物理刚体为多刚体组件,则此组件将对每个重叠的物理刚体将生成单独的重叠。设想一下骨架物理资源,具有其独立的碰撞形状。启用该项后,角色的手将生成 自己 的重叠事件。从而对所报告的内容及特定情况下应该怎么做有更多的控制。
  • Check Async Scene On Move:如果该项设置为 真 ,那么组件将在两个物理场景(同步和异步)中都查找碰撞。异步场景主要由可破坏网格物体的破碎块使用。
  • Trace Complex On Move:如果该项设置为 真, 扫过该组件的对象将在运动时跟踪复杂碰撞。复杂碰撞简而言之就是基于每个面的碰撞,而简单碰撞则是您的球体、胶囊体、盒体及生成的凸面体形状。
  • Return Material on Move:设置该项为 真 将返回物理材质到 Hit Info(碰撞信息) 中。

2、物理碰撞、重叠与包围盒

  • 有如图所示两个物理Mesh: Cube、Terrain
    这里写图片描述

2.1碰撞

  • 设置cube相关组件属性
    这里写图片描述
    • 启用Cube的物理模拟,使用重力。这样Cube将会受到重力而掉落。
    • 将Collision Enabled设置为Collision Enabled
    • 给Cube设置一个对象类型PhysicsBody(根据抽象逻辑设置)
    • 将对象响应类型(Object Type)的WorldStatic(Terrain的对象类型)设置为Block
  • 设置Terrain相关组件属性
    这里写图片描述
    • 将Collision Enabled设置为Collision Enabled
    • 给Terrain设置一个对象类型WorldStatic
    • 将对象响应类型的PhysicsBody(Cube的对象类型)设置为Block
  • 运行游戏
    这里写图片描述
    • 发现Cube落在了Terrain上面而没有穿过Terrain,是因为他们彼此设置了阻挡(Block)对方。
    • 由于Cube设置了Simulation Generates Hit Events为True,我们可以对该Mesh(Cube)使用如下方法进行事件响应。
//假设CubeMesh就是上图的Cube的UStaticMeshComponent组件
UStaticMeshComponent CubeMesh;
    CubeMesh->OnComponentHit.AddDynamic(this, &YourClass::OnHit);
    //注意:用于虚幻4引擎回调的函数都应使用UFUNCTION()修饰
    /*OnHit函数签名为:*/
    UFUNCTION()
        void OnHit(class AActor* OtherActor, class UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

2.2重叠

  • 我们现在仅需要在2.1的基础上对Cube和Terrain的对象响应类型做出少许修改即可。
  • 对Cube作出如下图所示的修改
    这里写图片描述
  • 对Terrain做出如下图所示的修改
    这里写图片描述
  • 运行游戏
    这里写图片描述
    • 我们发现Cube已经穿过Terrain
    • 当Cube和Terrain同时设置Generate Overlap Events为True时,如果两者中有一个对象设置为阻挡对方,另一个设置为重叠对方(或者两者都设置为重叠对方),那么我们就可以对该Mesh(Cube和Terrain)通过如下方法进行事件响应。
//假设CubeMesh就是上图的Cube的UStaticMeshComponent组件
UStaticMeshComponent CubeMesh;
CubeMesh->OnComponentBeginOverlap.AddDynamic(this, &YourClass::OnBeginOverlap);
CubeMesh->OnComponentEndOverlap.AddDynamic(this,&YourClass::OnEndOverlap);
/*OnBeginOverlap和OnEndOverlap的函数签名为:*/
//开始重叠
UFUNCTION()
    void OnBeginOverlap(class AActor* OtherActor, class UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
//结束重叠
UFUNCTION()
    void OnEndOverlap(class AActor* OtherActor, class UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

2.3包围盒

  • 基本概念
    包围盒(Unity中常成为碰撞盒)常用语碰撞检测,基本思想是用体积稍大且特性简单的几何体(称为包围盒)来近似地代替复杂的几何对象。
  • 添加包围盒,打开Mesh编辑器如下图所示
    这里写图片描述
    • 下拉列表中是一些包围盒的形状,添加了包围盒的Mesh才能进行物理模拟。

3、射线的使用

3.1自定义踪迹通道、对象通道、预设

  • 打开碰撞设置面板
    这里写图片描述->这里写图片描述->这里写图片描述
  • 新建如下图所示的对象通道和踪迹通道
    这里写图片描述

  • Object Channel和Trace Channel的可自定义通道总数为18个

  • 打开项目的DefaultsEngine.ini文件
    这里写图片描述
    • DefaultsEngine.ini
      这里写图片描述
    • 自定义通道与ECollisionChannel枚举类型映射关系
      可以看到,每一个新建的通道对应了一个ECC_GameTraceChannel,我们找到枚举类型ECollisionChannel的定义,由此,我们可以获得该枚举类型下的枚举值和我们自己定义的通道的映射。
      这里写图片描述

3.2使用射线

  • 基本场景
    这里写图片描述
  • 对自定义通道进行宏定义
//自定义踪迹通道的宏定义
#define CubeChannel     ECollisionChannel::ECC_GameTraceChannel6
#define SphereChannel   ECollisionChannel::ECC_GameTraceChannel5;
#define CylinderChannel ECollisionChannel::ECC_GameTraceChannel4
//自定义对象通道的宏定义
#define  CubeObject     ECollisionChannel::ECC_GameTraceChannel3
#define SphereObject    ECollisionChannel::ECC_GameTraceChannel2
#define  CylinderObject ECollisionChannel::ECC_GameTraceChannel1
  • 射线检测基本参数初始化
    /*射线的长度*/
    #define MAXRAYDISTANCE 100000
    //以下代码是写在APlayerController子类下的片段

    UWorld world = this->GetWorld();
    /*获取鼠标在屏幕上的坐标*/
    FVector2D mousePosition(0, 0);
    this->GetMousePosition(mousePosition.X, mousePosition.Y);

    FHitResult hitInfo;
    FVector rayWorldOrigin;
    FVector rayWorldDirection;
    /*获得屏幕上的坐标在世界坐标系下的位置,方向*/
    UGameplayStatics::DeprojectScreenToWorld(this, mousePosition, rayWorldOrigin, rayWorldDirection);

    FCollisionQueryParams queryParamInfo(false);

3.2.1根据踪迹通道进行检测

  • LineTraceSingleByChannel是根据踪迹通道来响应的,因此我们仅需要对踪迹通道进行如下设置,以Sphere为例(Cylinder,Cube设置同理),对象通道对射线踪迹通道追踪是没有影响的,故在此不用设置。
    这里写图片描述
  • 代码如下
//You always can change this variable to CubeChannel/CylinderChanneletc.
    ECollisionChannel TraceChanel = SphereChannel;
    if (world!=nullptr)
    {
        world->LineTraceSingleByChannel(hitInfo, rayWorldOrigin, rayWorldDirection * MAXRAYDISTANCE, TraceChanel, queryParamInfo);
    }
  • 运行游戏
    这里写图片描述
    • 当鼠标放在Sphere上时,射线碰撞到了Sphere,放在其他位置时是没有反应的。

3.2.2根据对象通道进行检测

  • LineTraceSingleByObjectType是根据对象类型(Object Type)来进行响应的,因此我们仅需要对Object Type进行设置,以Cube为例(Sphere,Cylinder同理)
    这里写图片描述

  • 代码如下

    FCollisionObjectQueryParams traceObjectTypeInfo;
    //traceObjectTypeInfo.AddObjectTypesToQuery(CubeObject);
    //traceObjectTypeInfo.AddObjectTypesToQuery(SphereObject);
    if (FCollisionObjectQueryParams::IsValidObjectQuery(CubeObject))
    {
        traceObjectTypeInfo.AddObjectTypesToQuery(CubeObject);
        //此处添加你要追踪的对象类型
    }
    if (world!=nullptr)
    {
        world->LineTraceSingleByObjectType(hitInfo, rayWorldOrigin, rayWorldDirection * MAXRAYDISTANCE, traceObjectTypeInfo, queryParamInfo);
    }
  • 运行游戏
    这里写图片描述
    • 当鼠标移动到Cube上,射线检测到了Cube,鼠标放到其他位置是没有反应的,因为仅仅添加了CubeObject用于检测。

3.2.3根据预设进行检测

  • 新建自定义预设CustomQuery
    这里写图片描述
  • 设置Cylinder碰撞属性
    • LineTraceSingleByProfile相当于赋予了射线拥有自己的对象类型,故在此我们设置相应物体的对象类型响应下WorldStatic为阻挡(Block),在此以Cylinder为例(Cube、Sphere同理)
      这里写图片描述
  • 代码如下
FName profileName(TEXT("CustomQuery"));
    if (world != nullptr)
    {
        world->LineTraceSingleByProfile(hitInfo, rayWorldOrigin, rayWorldDirection * MAXRAYDISTANCE, profileName,queryParamInfo);
    }
  • 运行游戏
    这里写图片描述

    • 由于CustomQuery设置了阻挡CylinderObject和CubeObject,故可看到射线检测到了Cylinder2和Cube,由于我把Cylinder1的对象类型响应下的WorldStatic响应设置为Igore,故在此没有检测到Cylinder1.
  • 总结:以上讲述了虚幻4引擎的物理碰撞、重叠、射线检测部分。其中射线检测部分主要围绕UWorld下的LineTraceSingleByChannel、LineTraceSingleByObjectType、LineTraceSingleByProfile三种检测方法进行检测。同时,在使用c++编程时还应该注意自定义踪迹通道、对象通道时,DefaultsEngine.ini配置文件中的ECollisionChannel
    枚举值与自定义通道的映射关系,然后用宏定义进行声明以便于代码的可读性。

  • 源代码文件下载:
2017-11-21 14:38:40 WAN_EXE 阅读数 1072
  • 从这里开始虚幻4-第2辑-蓝图 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2158 人正在学习 去看看 杨石兴

这个算是第一个比较深入到Unreal引擎编码的例子,这个例子中,可以通过A,S,D,W控制球体的移动,通过鼠标控制转向,有燃烧的火球,非常的逼真。

下面是运行的示例图,跑动的时候效果会更赞,可以看到燃烧的火球:

先贴上四个文件的代码,然后再来分析。

CollidingPawn.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Particles/ParticleSystemComponent.h"
#include "CollidingPawn.generated.h"


UCLASS()
class QUICKSTART_API ACollidingPawn : public APawn
{
	GENERATED_BODY()

public:
	// Sets default values for this pawn's properties
	ACollidingPawn();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	UParticleSystemComponent* OurParticleSystem;
	class UCollidingPawnMovementComponent* OurMovementComponent;

	virtual UPawnMovementComponent* GetMovementComponent() const override;

	void MoveForward(float AxisValue);
	void MoveRight(float AxisValue);
	void Turn(float AxisValue);
	void ParticleToggle();	
};

CollidingPawn.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "QuickStart.h"
#include "CollidingPawn.h"
#include "CollidingPawnMovementComponent.h"
#include "Components/SphereComponent.h"
#include "UObject/ConstructorHelpers.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"


// Sets default values
ACollidingPawn::ACollidingPawn()
{
 	// Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	// Our root component will be a sphere that reacts to physics
    USphereComponent* SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));
    RootComponent = SphereComponent;
    SphereComponent->InitSphereRadius(40.0f);
    SphereComponent->SetCollisionProfileName(TEXT("Pawn"));

    // Create and position a mesh component so we can see where our sphere is
    UStaticMeshComponent* SphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VisualRepresentation"));
    SphereVisual->SetupAttachment(RootComponent);
    static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
    if (SphereVisualAsset.Succeeded())
    {
        SphereVisual->SetStaticMesh(SphereVisualAsset.Object);
        SphereVisual->SetRelativeLocation(FVector(0.0f, 0.0f, -40.0f));
        SphereVisual->SetWorldScale3D(FVector(0.8f));
    }

    // Create a particle system that we can activate or deactivate
    OurParticleSystem = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("MovementParticles"));
    OurParticleSystem->SetupAttachment(SphereVisual);
    OurParticleSystem->bAutoActivate = false;
    OurParticleSystem->SetRelativeLocation(FVector(-20.0f, 0.0f, 20.0f));
    static ConstructorHelpers::FObjectFinder<UParticleSystem> ParticleAsset(TEXT("/Game/StarterContent/Particles/P_Fire.P_Fire"));
    if (ParticleAsset.Succeeded())
    {
        OurParticleSystem->SetTemplate(ParticleAsset.Object);
    }

    // Use a spring arm to give the camera smooth, natural-feeling motion.
    USpringArmComponent* SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraAttachmentArm"));
    SpringArm->SetupAttachment(RootComponent);
    SpringArm->RelativeRotation = FRotator(-45.f, 0.f, 0.f);
    SpringArm->TargetArmLength = 400.0f;
    SpringArm->bEnableCameraLag = true;
    SpringArm->CameraLagSpeed = 3.0f;

    // Create a camera and attach to our spring arm
    UCameraComponent* Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("ActualCamera"));
    Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);

    // Take control of the default player
    AutoPossessPlayer = EAutoReceiveInput::Player0;

	// Create an instance of our movement component, and tell it to update our root component.
    OurMovementComponent = CreateDefaultSubobject<UCollidingPawnMovementComponent>(TEXT("CustomMovementComponent"));
    OurMovementComponent->UpdatedComponent = RootComponent;
}

// Called when the game starts or when spawned
void ACollidingPawn::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void ACollidingPawn::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void ACollidingPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	InputComponent->BindAction("ParticleToggle", IE_Pressed, this, &ACollidingPawn::ParticleToggle);

    InputComponent->BindAxis("MoveForward", this, &ACollidingPawn::MoveForward);
    InputComponent->BindAxis("MoveRight", this, &ACollidingPawn::MoveRight);
    InputComponent->BindAxis("Turn", this, &ACollidingPawn::Turn);
}

UPawnMovementComponent* ACollidingPawn::GetMovementComponent() const
{
    return OurMovementComponent;
}

void ACollidingPawn::MoveForward(float AxisValue)
{
    if (OurMovementComponent && (OurMovementComponent->UpdatedComponent == RootComponent))
    {
        OurMovementComponent->AddInputVector(GetActorForwardVector() * AxisValue);
    }
}

void ACollidingPawn::MoveRight(float AxisValue)
{
    if (OurMovementComponent && (OurMovementComponent->UpdatedComponent == RootComponent))
    {
        OurMovementComponent->AddInputVector(GetActorRightVector() * AxisValue);
    }
}

void ACollidingPawn::Turn(float AxisValue)
{
    FRotator NewRotation = GetActorRotation();
    NewRotation.Yaw += AxisValue;
    SetActorRotation(NewRotation);
}

void ACollidingPawn::ParticleToggle()
{
    if (OurParticleSystem && OurParticleSystem->Template)
    {
        OurParticleSystem->ToggleActive();
    }
}

CollidingPawnMovementComponent.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PawnMovementComponent.h"
#include "CollidingPawnMovementComponent.generated.h"

/**
 * 
 */
UCLASS()
class QUICKSTART_API UCollidingPawnMovementComponent : public UPawnMovementComponent
{
	GENERATED_BODY()
	
public:
	virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;	
};

CollidingPawnMovementComponent.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PawnMovementComponent.h"
#include "CollidingPawnMovementComponent.generated.h"

/**
 * 
 */
UCLASS()
class QUICKSTART_API UCollidingPawnMovementComponent : public UPawnMovementComponent
{
	GENERATED_BODY()
	
public:
	virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;	
};


2014-09-26 01:11:21 shangguanwaner 阅读数 3329
  • 从这里开始虚幻4-第2辑-蓝图 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2158 人正在学习 去看看 杨石兴

游戏开发中经常会用到射线碰撞,比如激光器打一枪,需要明确知道它集中的位置,然后在这个点释放攻击特效。

Unrea Engine 4中做射线碰撞也很简单,主要功能的实现是World的LineTraceSingle这个方法,下面给出测试代码。代码我在ThirdPersonTemplate中测试。检测角色正前方有无柯碰撞的Actor,有就在碰撞点上显示一个调试用的圆球。代码如下:

void ANanProjectCharacter::Raycast()
{
	FHitResult hitResult(ForceInit); 
	FVector pos, dir;
	FCollisionQueryParams ccq(FName(TEXT("CombatTrace")), true, NULL);
	ccq.bTraceComplex = true;
	ccq.bTraceAsyncScene = false;
	ccq.bReturnPhysicalMaterial = false;
	ccq.AddIgnoredActor(this);

	pos = GetActorLocation();

	const FRotator Rotation = CapsuleComponent->GetComponentRotation();
	const FRotator YawRotation(0, Rotation.Yaw, 0);

	// get forward vector
	dir = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);

	FVector posBegin = pos;
	FVector posEnd = pos + dir * 500;

	GetWorld()->LineTraceSingle(hitResult,
		posBegin,
		posEnd,
		ECC_WorldStatic,
		ccq);

	DrawDebugLine(this->GetWorld(), posBegin, posEnd, FColor(1.0f, 0.f, 0.f, 1.f), false, 20.f);
	if (hitResult.GetActor())
	{
		DrawDebugSphere(GetWorld(), hitResult.Location, 10, 10, FColor::Red, false, 20.f);
		
	}
}


注意下,需要把角色自己从碰撞检测中排除掉。最终效果如下:



没有更多推荐了,返回首页