UE4 C++代码实现电池人游戏

2022年05月14日 阅读数:2
这篇文章主要向大家介绍UE4 C++代码实现电池人游戏,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

目录html

1.新建项目git

2 功能实现编辑器

2.1 在BattetyMan.h中添加以下头文件:ide

2.2 添加属性函数

2.3 添加移动的函数ui

2.4 添加能够捡起的小球this

2.5 UI的编写spa

2.6 自动生成小球3d

3 详细代码code


1.新建项目

新建BatteryMan类

须要继承自Character

2 功能实现

2.1 在BattetyMan.h中添加以下头文件:

#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
#include "Blueprint/UserWidget.h"

须要注意的是,这些头文件须要添加在#include "BatteryMan.generated.h" 以前,不然会编译出错。

Camera表明玩家的视角,能够理解为玩家如何看待这个世界,CameraComponent则提供关于相机的属性信息。

Components是组件的意思,CapsuleComponent是一个一般用于简单碰撞的胶囊。

StaticMeshComponent是静态网格组件,是由一组静态的多边形组成的几何体。

InputComponent用于实现一个用于输入绑定的Actor组件。

CharacterMovementComponent处理相关角色全部者的运动逻辑。

Controller是Pawn的灵魂,用来控制Pawn的行为。

SpringArmComponent将子类与父代保持在一个固定的距离上,若是中间有遮挡,它将切换为子类,而在没有碰撞时则弹回。

UserWidget是用户界面UI

SprintArmComponent可能难以理解,以下图所示,当没有遮挡的时候镜头是这样:

当人物被遮挡的时候,镜头会自动切到人物上:

2.2 添加属性

在添加属性以前首先介绍一下UPROPERTY:

声明属性时,属性说明符 可被添加到声明,以控制属性与引擎和编辑器诸多方面的相处方式。

使用方式:

UPROPERTY(属性标签, 属性元标签,类别)

属性常见的标签有:

VisibleAnywhere:说明此属性在全部属性窗口中可见,但没法被编辑。此说明符与"Edit"说明符不兼容。

BlueprintReadOnly:此属性可由蓝图读取,但不能被修改。

EditAnywhere:说明此属性可经过属性窗口在原型和实例上进行编辑。此说明符与全部"可见"说明符不可同时用。

更多详细的属性参考官方文档:https://docs.unrealengine.com/zh-CN/ProgrammingAndScripting/GameplayArchitecture/Properties/Specifiers/index.html

了解了UPROPERTY属性后咱们在头文件中添加以下属性:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
		USpringArmComponent* CameraBoom;

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
		UCameraComponent* FollowCamera;

SpringArmComponent将子类与父代保持在一个固定的距离上,若是中间有遮挡,它将切换为子类,而在没有碰撞时则弹回。

UCameraComponent是摄像机组件,将其挂载到SpringArmComponent之下,这样就能够实现当发生遮挡,视角切换的功能。

在UE中新建文件夹BluePrints,并在BluePrints中右键新建BatteryMan的蓝图类

双击打开刚刚新建的蓝图类,选择Mesh的Skeleten

旋转任务,将任务朝向对准为Arrow的方向

让后调整人物的位置,以及胶囊体的大小,把人物放到胶囊体内:

为了让这个蓝图类更具备通用性,咱们在BatteryMan的构造函数BatteryMan::ABatteryMan()中设置胶囊体的大小:

GetCapsuleComponent()->InitCapsuleSize(42.0f, 96.0f);

在旋转的时候咱们但愿镜头旋转,而不是人物旋转,因此这里须要把用户的旋转功能关闭:

bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;

在构造函数ABatteryMan::ABatteryMan()中将摄像机组件初始化

	GetCharacterMovement()->bOrientRotationToMovement = true;  //若是为真,则使用相机方向旋转角色,使用RotationRate做为角色旋转变化的速率。
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 600.0f, 0.0f);
	GetCharacterMovement()->JumpZVelocity = 900.0f;
	GetCharacterMovement()->AirControl = 0.0f;//在空中控制角色的灵敏度

	//建立USpringArmComponent组件
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->SetupAttachment(RootComponent);

	CameraBoom->TargetArmLength = 300.0f;//300 cm
	CameraBoom->bUsePawnControlRotation = true;//运动时控制视角

	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
	//附加到CameraBoom上
	FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);

编译后在蓝图中会出现以下组件:

右键新建GameMode C++类

新建该类的蓝图,这里蓝图类的名称为BatteryMan_GameMode_BP:

接下来进行以下设置,设置游戏开始控制的角色蓝图类以及游戏操做模式的蓝图类:

设置完之后运行游戏,会出现以下:

添加 bDead属性,并在构造函数中设置为false,表示目标是否死亡。

bool bDead;

2.3 添加移动的函数

void MoveForward(float Axis);
void MoveRight(float Axis);
void ABatteryMan::MoveForward(float Axis)
{
	if (!bDead)
	{
		const FRotator rotation = Controller->GetControlRotation();
		const FRotator YawRotation(0, rotation.Yaw, 0);//Z轴旋转的角度

		const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);

		AddMovementInput(Direction, Axis);
	}
}

void ABatteryMan::MoveRight(float Axis)
{
	if (!bDead)
	{
		const FRotator rotation = Controller->GetControlRotation();
		const FRotator YawRotation(0, rotation.Yaw, 0);

		const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

		AddMovementInput(Direction, Axis);
	}
}

在BatteryMan::SetupPlayerInputComponent函数中绑定输入动做:

// Called to bind functionality to input
void ABatteryMan::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
	PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);

	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
	PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);

	PlayerInputComponent->BindAxis("MoveForward", this, &ABatteryMan::MoveForward);
	PlayerInputComponent->BindAxis("MoveRight", this, &ABatteryMan::MoveRight);
}

实现运动:

void ABatteryMan::MoveForward(float Axis)
{
	if (!bDead)
	{
		const FRotator rotation = Controller->GetControlRotation();

		const FVector Direction = FRotationMatrix(rotation).GetUnitAxis(EAxis::X);

		AddMovementInput(Direction, Axis);
	}
}

void ABatteryMan::MoveRight(float Axis)
{
	//UE_LOG(LogTemp, Warning, TEXT("MoveRight %f"),Axis);
	if (!bDead)
	{
		const FRotator rotation = Controller->GetControlRotation();

		const FVector Direction = FRotationMatrix(rotation).GetUnitAxis(EAxis::Y);

		AddMovementInput(Direction, Axis);
	}
}

添加加速功能,在input中 添加输入:

在BatteryMan.h中添加

void Accelerate();
void DeAccelerate();

并在ABatteryMan::SetupPlayerInputComponent绑定事件:


	PlayerInputComponent->BindAction("Accelerate", IE_Pressed, this, &ABatteryMan::Accelerate);
	PlayerInputComponent->BindAction("Accelerate", IE_Released, this, &ABatteryMan::DeAccelerate);

实现函数:


void ABatteryMan::Accelerate()
{

	GetCharacterMovement()->MinAnalogWalkSpeed = 6000;
	

}

void ABatteryMan::DeAccelerate()
{
	GetCharacterMovement()->MinAnalogWalkSpeed = 0;
}

2.4 添加能够捡起的小球

右键添加蓝图类,基类为Actor,并取名为PickbleItem_BP

打开PickbleItem_BP并添加球体Sphere,与球体碰撞体,调整好大小并设置球体的Tag为Recharge,用于在代码中判断是否触碰到小球:

另一个须要设置的地方是把小球的Simulate Pysics属性勾上

在BatteryMan.h中添加 

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
		float Power;//表示当前的电量

	UPROPERTY(EditAnywhere)
		float Power_Threshold;//表示每隔必定时间减小多少电量

继续添加处理碰撞的UFUNCTION函数

	//HitComp:碰撞到了什么组件
	//OtherActor:碰撞到的Actor
	UFUNCTION()
		void OnBeginOverlap(class UPrimitiveComponent* HitComp,
			class AActor* OtherActor, class UPrimitiveComponent* OtherComp,
			int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

设置碰撞响应

在CPP中实现碰撞函数:

void ABatteryMan::OnBeginOverlap(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	UE_LOG(LogTemp, Warning, TEXT("Collided with"));
	if (OtherActor->ActorHasTag("Recharge"))
	{
		

		Power += 10.0f;

		if (Power > 100.0f)
			Power = 100.0f;

		
		OtherActor->Destroy();
	}
}

在开始游戏时绑定碰撞函数:

void ABatteryMan::BeginPlay()
{
	Super::BeginPlay();

	GetCapsuleComponent()->OnComponentBeginOverlap.AddDynamic(this, &ABatteryMan::OnBeginOverlap);
}

2.5 UI的编写

右键新建界面蓝图类,并命名为Player_Power_UI

打开ui并添加Progress Bar

在BatteryMan.h中添加Widget属性

	UPROPERTY(EditAnywhere, Category = "UI HUD")
		TSubclassOf<UUserWidget> Player_Power_Widget_Class;
	UUserWidget* Player_Power_Widget;

在Battery_Collector.Build.cs添加UMG支持

编译后,在BatteryMan能够看到UI_HUB的属性,将类设置为刚刚新建的蓝图类UI:

在BatteryMan的BeginPlay中添加以下代码:

	if (Player_Power_Widget_Class != nullptr) {
		Player_Power_Widget = CreateWidget(GetWorld(), Player_Power_Widget_Class);
		Player_Power_Widget->AddToViewport();
	}

在UI界面中绑定ProgressBar的槽函数

并作以下的操做:

Tick是每一帧都会执行的函数,在Tick函数中对Power进行递减:

若是Power小于等于0则游戏结束,并从新开始游戏,添加头文件,并在Tick函数中判断是否死亡:

#include "BatteryMan.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/Actor.h"
#include "Kismet/GameplayStatics.h"

此外还须要对角色作以下设置,使得在SimulatePhysics为True的时候角色倒下:

首先新建Physics文件夹,并在该文件夹内新建Physics Asset蓝图类,并命名为MyRagDollFemale

选择:

在BatteryMan_BP中对该类进行设置:

添加剧新开始游戏的方法RestartGame

void ABatteryMan::RestartGame()
{
	UGameplayStatics::OpenLevel(this, FName(*GetWorld()->GetName()),false);
}

补充Tick()函数:

2.6 自动生成小球

在BatteryMan_GameMode.h中添加以下函数:

UCLASS()
class CUETEST_API ABatteryMan_GameMode : public AGameMode
{
	GENERATED_BODY()
	ABatteryMan_GameMode();

	virtual void BeginPlay() override;
	
	virtual void Tick(float DeltaSeconds) override;

	UPROPERTY(EditAnywhere)
		TSubclassOf<APawn> PlayerRecharge;

	float SPawn_Z = 500.0f;

	UPROPERTY(EditAnywhere)
		float SPawn_X_Min;

	UPROPERTY(EditAnywhere)
		float SPawn_X_Max;

	UPROPERTY(EditAnywhere)
		float SPawn_Y_Min;

	UPROPERTY(EditAnywhere)
		float SPawn_Y_Max;

	void SpawnPlaerRecharge();
};

经过俯视图获得各个方向的最大值最小值,随便拖一个组件上去,而后拖动查看位置:

在GameMode蓝图类中作以下设置:

在BatteryMan_GameMode_BP中对PlayerRecharge进行初始化

BatteryMan_GameMode.cpp中添加实现:

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


#include "BatteryMan_GameMode.h"
#include "GameFramework/Actor.h"

ABatteryMan_GameMode::ABatteryMan_GameMode()
{
	PrimaryActorTick.bCanEverTick = true;
}

void ABatteryMan_GameMode::BeginPlay()
{
	Super::BeginPlay();

	FTimerHandle UnsedHandle;
	GetWorldTimerManager().SetTimer(UnsedHandle, this, &ABatteryMan_GameMode::SpawnPlaerRecharge, FMath::RandRange(1,3), true);

}

void ABatteryMan_GameMode::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);
}

void ABatteryMan_GameMode::SpawnPlaerRecharge()
{
	float RandX = FMath::RandRange(SPawn_X_Min, SPawn_X_Max);
	float RandY = FMath::RandRange(SPawn_Y_Min, SPawn_Y_Max);

	FVector SpawnPosition = FVector(RandX, RandY, SPawn_Z);
	FRotator SpawnRotation = FRotator(0.0f, 0.0f, 0.0f);

	GetWorld()->SpawnActor(PlayerRecharge,&SpawnPosition,&SpawnRotation);
}

至此,游戏已经完成

3 详细代码

BatteryMan.h

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"

#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
#include "Blueprint/UserWidget.h"

#include "BatteryMan.generated.h"

UCLASS()
class CUETEST_API ABatteryMan : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	ABatteryMan();


	void MoveForward(float Axis);
	void MoveRight(float Axis);

	void Accelerate();
	void DeAccelerate();

	bool bDead;


	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
		USpringArmComponent* CameraBoom;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
		UCameraComponent* FollowCamera;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
		float Power;//表示当前的电量

	UPROPERTY(EditAnywhere)
		float Power_Threshold;//表示吃一个小球涨多少电量

	//HitComp:碰撞到了什么组件
	//OtherActor:碰撞到的Actor
	UFUNCTION()
		void OnBeginOverlap(class UPrimitiveComponent* HitComp,
			class AActor* OtherActor, class UPrimitiveComponent* OtherComp,
			int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);


	UPROPERTY(EditAnywhere, Category = "UI HUD")
		TSubclassOf<UUserWidget> Player_Power_Widget_Class;
	UUserWidget* Player_Power_Widget;

	void RestartGame();


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;

};

BatteryMan.cpp

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


#include "BatteryMan.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/Actor.h"
#include "Kismet/GameplayStatics.h"

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

	GetCapsuleComponent()->InitCapsuleSize(42.0f, 96.0f);

	//bUseControllerRotationPitch = false;
	//bUseControllerRotationYaw = false;
	//bUseControllerRotationRoll = false;

	GetCharacterMovement()->bOrientRotationToMovement = true;  //若是为真,则使用相机方向旋转角色,使用RotationRate做为角色旋转变化的速率。
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 600.0f, 0.0f);
	GetCharacterMovement()->JumpZVelocity = 900.0f;
	GetCharacterMovement()->AirControl = 0.0f;//在空中控制角色的灵敏度

	//建立USpringArmComponent组件
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->SetupAttachment(RootComponent);

	CameraBoom->TargetArmLength = 300.0f;//300 cm
	CameraBoom->bUsePawnControlRotation = true;//运动时控制视角

	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
	//附加到CameraBoom上
	FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);

	bDead = false;

	Power = 100;

	Power_Threshold = 1;
}





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

	GetCapsuleComponent()->OnComponentBeginOverlap.AddDynamic(this, &ABatteryMan::OnBeginOverlap);

	if (Player_Power_Widget_Class != nullptr) {
		Player_Power_Widget = CreateWidget(GetWorld(), Player_Power_Widget_Class);
		Player_Power_Widget->AddToViewport();
	}
	
}

void ABatteryMan::RestartGame()
{
	UGameplayStatics::OpenLevel(this, FName(*GetWorld()->GetName()),false);
}

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

	Power -= DeltaTime * Power_Threshold;

	if (Power <= 0) {
		bDead = true;

		//游戏结束
		GetMesh()->SetSimulatePhysics(true);

		FTimerHandle UnsedHandle;
		GetWorldTimerManager().SetTimer(UnsedHandle, this, &ABatteryMan::RestartGame, 3.0f, false);

	}

}

// Called to bind functionality to input
void ABatteryMan::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
	PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);

	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
	PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);

	PlayerInputComponent->BindAction("Accelerate", IE_Pressed, this, &ABatteryMan::Accelerate);
	PlayerInputComponent->BindAction("Accelerate", IE_Released, this, &ABatteryMan::DeAccelerate);

	PlayerInputComponent->BindAxis("MoveForward", this, &ABatteryMan::MoveForward);
	PlayerInputComponent->BindAxis("MoveRight", this, &ABatteryMan::MoveRight);
}

void ABatteryMan::OnBeginOverlap(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	UE_LOG(LogTemp, Warning, TEXT("Collided with"));
	if (OtherActor->ActorHasTag("Recharge"))
	{
		

		Power += 10.0f;

		if (Power > 100.0f)
			Power = 100.0f;

		
		OtherActor->Destroy();
	}
}

void ABatteryMan::MoveForward(float Axis)
{
	if (!bDead)
	{
		const FRotator rotation = Controller->GetControlRotation();

		const FVector Direction = FRotationMatrix(rotation).GetUnitAxis(EAxis::X);

		AddMovementInput(Direction, Axis);
	}
}

void ABatteryMan::MoveRight(float Axis)
{
	//UE_LOG(LogTemp, Warning, TEXT("MoveRight %f"),Axis);
	if (!bDead)
	{
		const FRotator rotation = Controller->GetControlRotation();

		const FVector Direction = FRotationMatrix(rotation).GetUnitAxis(EAxis::Y);

		AddMovementInput(Direction, Axis);
	}
}

void ABatteryMan::Accelerate()
{

	GetCharacterMovement()->MinAnalogWalkSpeed = 6000;
	

}

void ABatteryMan::DeAccelerate()
{
	GetCharacterMovement()->MinAnalogWalkSpeed = 0;
}

BatteryMan_GameMode.h

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "BatteryMan_GameMode.generated.h"

/**
 * 
 */
UCLASS()
class CUETEST_API ABatteryMan_GameMode : public AGameMode
{
	GENERATED_BODY()
	ABatteryMan_GameMode();

	virtual void BeginPlay() override;
	
	virtual void Tick(float DeltaSeconds) override;

	UPROPERTY(EditAnywhere)
		TSubclassOf<APawn> PlayerRecharge;

	float SPawn_Z = 500.0f;

	UPROPERTY(EditAnywhere)
		float SPawn_X_Min;

	UPROPERTY(EditAnywhere)
		float SPawn_X_Max;

	UPROPERTY(EditAnywhere)
		float SPawn_Y_Min;

	UPROPERTY(EditAnywhere)
		float SPawn_Y_Max;

	void SpawnPlaerRecharge();
};

BatteryMan_GameMode.cpp

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


#include "BatteryMan_GameMode.h"
#include "GameFramework/Actor.h"

ABatteryMan_GameMode::ABatteryMan_GameMode()
{
	PrimaryActorTick.bCanEverTick = true;
}

void ABatteryMan_GameMode::BeginPlay()
{
	Super::BeginPlay();

	FTimerHandle UnsedHandle;
	GetWorldTimerManager().SetTimer(UnsedHandle, this, &ABatteryMan_GameMode::SpawnPlaerRecharge, FMath::RandRange(1,3), true);

}

void ABatteryMan_GameMode::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);
}

void ABatteryMan_GameMode::SpawnPlaerRecharge()
{
	float RandX = FMath::RandRange(SPawn_X_Min, SPawn_X_Max);
	float RandY = FMath::RandRange(SPawn_Y_Min, SPawn_Y_Max);

	FVector SpawnPosition = FVector(RandX, RandY, SPawn_Z);
	FRotator SpawnRotation = FRotator(0.0f, 0.0f, 0.0f);

	GetWorld()->SpawnActor(PlayerRecharge,&SpawnPosition,&SpawnRotation);
}

CUETest.Build.cs

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class CUETest : ModuleRules
{
	public CUETest(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay","UMG" });
	}
}

 代码连接:CodingInn/UE4 Basic Tutorial