对于初学者来说,UE的项目案例是我们入手的最佳途径,首先代码量少,思路清晰,还能给你提供一个清晰的结构。所以,我创建了一个俯视角的官方案例,来查看一下官方的代码学习一下。

首先打开引擎,启动引擎

UE5俯视角游戏案例代码查看_ue5


然后创建一个示例,这是ue自带的案例

UE5俯视角游戏案例代码查看_自定义_02


打开源代码,看到案例就几个文件

UE5俯视角游戏案例代码查看_sed_03

TopDownProject

这个项目名称我设置的是TopDownProject,TopDownProject.h和TopDownProject.cpp就是自动生成的项目主文件,它不需要我们自己创建,案例中里面这两个文件主要在里面增加了一个打印日志的宏,宏的第一个参数是定义了名称,可以不一样,但是相同的不需要定义两遍。这样方便后面查看哪里报错。

DECLARE_LOG_CATEGORY_EXTERN(LogTopDownProject, Log, All);
DEFINE_LOG_CATEGORY(LogTopDownProject)

定义完宏以后,使用,需要使用UE_LOG去打印,这里我将输入映射上下文设置为空指针,然后打印

if(DefaultMappingContext == nullptr)
	{
		UE_LOG(LogTemplateCharacter, Error, TEXT("当前操作映射上下文未设置"));
	}

可以看到在输出日志里面显示打印的内容
ELogVerbosity 枚举类型通常包含以下几个级别(具体级别可能因UE版本而异):

  • Verbose:最详细的日志级别,通常用于调试目的,包含大量的信息。
  • Log:常规日志级别,用于记录程序运行时的正常消息。
  • Warning:警告级别,用于记录可能导致问题的情况,但不一定是错误。
  • Error:错误级别,用于记录程序运行时遇到的严重问题或异常。
  • Display:用于显示给用户的信息,通常出现在用户界面上。
  • Fatal:致命错误级别,通常用于记录程序无法继续运行的情况。

GameMode

案例重新创建了一个GameMode

UE5俯视角游戏案例代码查看_游戏_04


它文件头设置了minimalapi,是为了加快编译,也无法作为蓝图父类,作为直接设置无法修改的类。

UCLASS(minimalapi)

在里面只是在构造函数内做了一些处理

public:
	ATopDownProjectGameMode();

因为无法在UE里面去修改内容,所以它在构造函数内,重新设置了默认Pawn类和玩家控制器类,可以通过上图看到。这里面也教给我们如何在C++里面去获取对应的蓝图的方法。你也可以看到官方开发人员也写的不标准,下面的NULL虽然也不会出错,但是推荐修改为nullptr(空指针)

ATopDownProjectGameMode::ATopDownProjectGameMode()
{
	// 使用我们自定义的 PlayerController class
	PlayerControllerClass = ATopDownProjectPlayerController::StaticClass();

	// 设置默认的控制Pawn为我们自定义的蓝图创建的Character
	static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/TopDown/Blueprints/BP_TopDownCharacter"));
	if (PlayerPawnBPClass.Class != nullptr)
	{
		DefaultPawnClass = PlayerPawnBPClass.Class;
	}

	// 设置控制器为我们创建蓝图PlayerController
	static ConstructorHelpers::FClassFinder<APlayerController> PlayerControllerBPClass(TEXT("/Game/TopDown/Blueprints/BP_TopDownPlayerController"));
	if(PlayerControllerBPClass.Class != NULL)
	{
		PlayerControllerClass = PlayerControllerBPClass.Class;
	}
}

剩下两个文件刚好是一个角色类和一个玩家控制类。

Character

在自定义角色类这里,类继承至ACharacter
在类里面创建了两个私有变量,用于存储相机和弹簧臂

private:
	/** Top down camera */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class UCameraComponent* TopDownCameraComponent;

	/** Camera boom positioning the camera above the character */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class USpringArmComponent* CameraBoom;

meta 这个参数用于控制属性在引擎和编辑器之间的交互方式。它还有其他设置的内容:

  • meta = (AllowPrivateAccess = “true”) 为变量即使是私有属性,也可以在UE里编辑器和访问
  • meta=(DisplayName=“自定义名称”) 用于在UE中自定义属性的显示名称
  • meta=(ToolTip=“这是一个提示信息”) 为属性提供编辑器中的工具提示,帮助解释属性的用途或如何设置它
  • meta=(EditCondition=“bSomeCondition”) 定义一个布尔表达式,用于控制属性是否可以在UE中编辑
  • meta=(ClampMin=“0.0”, ClampMax=“100.0”) 用于限制编辑器中可编辑属性的最小值和最大值
  • meta=(Category=“MyCustomCategory”) 指定属性在编辑器细节面板中所属的类别
  • meta=(AdvancedDisplay) 用于将属性标记为高级属性,使其在编辑器中默认隐藏,但可以通过点击“显示更多”或类似按钮来显示

然后又增加了两个对属性的获取公共函数,FORCEINLINE 内联函数是在调用点直接插入函数体代码的函数,而不是进行常规的函数调用。这可以减少函数调用的开销,从而可能提高执行速度,但也可能增加生成的代码大小。

/** Returns TopDownCameraComponent subobject **/
	FORCEINLINE class UCameraComponent* GetTopDownCameraComponent() const { return TopDownCameraComponent; }
	/** Returns CameraBoom subobject **/
	FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }

在TopDownProjectCharacter的cpp文件中,只是在构造函数中初始化了一些内容,首先设置角色的胶囊体,然后设置角色移动相关的内容,并创建了相机弹簧臂和相机,最后设置帧回调。

ATopDownProjectCharacter::ATopDownProjectCharacter()
{
	// 设置角色胶囊体的尺寸
	GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

	// 禁止相机随角色旋转
	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	// Configure character movement
	GetCharacterMovement()->bOrientRotationToMovement = true; // 当设置为true时,角色的前方将自动朝向其移动的方向
	GetCharacterMovement()->RotationRate = FRotator(0.f, 640.f, 0.f); //控制角色旋转的速率
	GetCharacterMovement()->bConstrainToPlane = true; //当设置为true时,角色的移动将被约束在一个特定的平面上,通常是地面。
	GetCharacterMovement()->bSnapToPlaneAtStart = true; //游戏开始时,角色被吸附到地面,防止有空中坠落或者卡在地面的问题

	// 创建相机弹簧臂
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom")); //创建弹簧臂
	CameraBoom->SetupAttachment(RootComponent); //附加到根组件上
	CameraBoom->SetUsingAbsoluteRotation(true); // 不跟随根组件旋转
	CameraBoom->TargetArmLength = 800.f; //设置弹簧臂长度
	CameraBoom->SetRelativeRotation(FRotator(-60.f, 0.f, 0.f)); //设置弹簧臂的角度
	CameraBoom->bDoCollisionTest = false; // 设置为false,弹簧臂将不会与其他碰撞体产生交互

	// 创建相机
	TopDownCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("TopDownCamera")); //创建相机组件
	TopDownCameraComponent->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); //附加到弹簧臂上面
	TopDownCameraComponent->bUsePawnControlRotation = false; // 设置为false,相机的旋转不受角色控制

	// 激活帧更新,以便每帧更新光标
	PrimaryActorTick.bCanEverTick = true; //设置是否帧更新,如果当前值为false,即使设置其它,也不会被更新
	PrimaryActorTick.bStartWithTickEnabled = true; //游戏开始时,是否立即开始帧回调,如果bCanEverTick为false,也无法帧回调
}

这就是整个Character的内容,其实都是在蓝图里面都可以设置的东西,只不过修改成了使用c++去设置。

PlayerController

在PlayerController里面,首先增加了一个宏用于打印调试,和前面引擎文件里的一样,只是名称不一样,方便区分。

DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);

然后在类内部设置了构造函数,定义了一些可编辑变量,比如点击地面时生成的箭头特效,设置一个时间阈值判断是点击事件还是长按事件,增强输入的上下文,还有点击的action(鼠标和触摸屏的)

public:
	ATopDownProjectPlayerController();

	/** 定义一个时间是长按还是点击 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
	float ShortPressThreshold;

	/** 点击地面生成的箭头特效 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
	UNiagaraSystem* FXCursor;

	/** MappingContext */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input, meta=(AllowPrivateAccess = "true"))
	UInputMappingContext* DefaultMappingContext;
	
	/** 鼠标点击 Input Action */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input, meta=(AllowPrivateAccess = "true"))
	UInputAction* SetDestinationClickAction;

	/** 触屏点击 Input Action */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input, meta=(AllowPrivateAccess = "true"))
	UInputAction* SetDestinationTouchAction;

设置完成,可以在UE面板里面去修改对应的配置

UE5俯视角游戏案例代码查看_游戏_05

接着覆盖BeginPlay函数,这个函数在开始运行时触发

virtual void BeginPlay();

在实现这里,获取增强输入的子系统,添加自定义的输入映射上下文

void ATopDownProjectPlayerController::BeginPlay()
{
	// Call the base class  
	Super::BeginPlay();

	//Add Input Mapping Context
	if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
	{
		Subsystem->AddMappingContext(DefaultMappingContext, 0);
	}
}

然后覆盖SetupInputComponent,这个函数允许我们自定义输入

virtual void SetupInputComponent() override;

在函数实现这里,绑定了InputAction的按下,悬停,抬起,取消四个事件(分别绑定了鼠标和触摸,兼容移动端)

void ATopDownProjectPlayerController::SetupInputComponent()
{
	// set up gameplay key bindings
	Super::SetupInputComponent();

	// Set up action bindings
	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(InputComponent))
	{
		// Setup mouse input events
		EnhancedInputComponent->BindAction(SetDestinationClickAction, ETriggerEvent::Started, this, &ATopDownProjectPlayerController::OnInputStarted);
		EnhancedInputComponent->BindAction(SetDestinationClickAction, ETriggerEvent::Triggered, this, &ATopDownProjectPlayerController::OnSetDestinationTriggered);
		EnhancedInputComponent->BindAction(SetDestinationClickAction, ETriggerEvent::Completed, this, &ATopDownProjectPlayerController::OnSetDestinationReleased);
		EnhancedInputComponent->BindAction(SetDestinationClickAction, ETriggerEvent::Canceled, this, &ATopDownProjectPlayerController::OnSetDestinationReleased);

		// Setup touch input events
		EnhancedInputComponent->BindAction(SetDestinationTouchAction, ETriggerEvent::Started, this, &ATopDownProjectPlayerController::OnInputStarted);
		EnhancedInputComponent->BindAction(SetDestinationTouchAction, ETriggerEvent::Triggered, this, &ATopDownProjectPlayerController::OnTouchTriggered);
		EnhancedInputComponent->BindAction(SetDestinationTouchAction, ETriggerEvent::Completed, this, &ATopDownProjectPlayerController::OnTouchReleased);
		EnhancedInputComponent->BindAction(SetDestinationTouchAction, ETriggerEvent::Canceled, this, &ATopDownProjectPlayerController::OnTouchReleased);
	}
	else
	{
		UE_LOG(LogTemplateCharacter, Error, TEXT("'%s' Failed to find an Enhanced Input Component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."), *GetNameSafe(this));
	}
}

接下来就是定义上面绑定的函数,以及对应所需的变量,就是通过这里实现的角色移动,接下来,我们看一下是如何实现移动的。

/** Input handlers for SetDestination action. */
	void OnInputStarted();
	void OnSetDestinationTriggered();
	void OnSetDestinationReleased();
	void OnTouchTriggered();
	void OnTouchReleased();

private:
	FVector CachedDestination; //存储鼠标点击的位置

	bool bIsTouch; // 是否开启屏幕触摸
	float FollowTime; // 用于查看按住了多久

在鼠标按下时,会触发OnInputStarted()函数,这个函数内调用StopMovement()函数,停止角色移动。

void ATopDownProjectPlayerController::OnInputStarted()
{
	StopMovement();
}

鼠标按住,会触发角色跟随鼠标移动事件,在这种模式下,角色会直接朝向鼠标移动,不会自动躲避障碍物,我们看一下悬停回调函数的实现。
函数内先记录一下FollowTime 就是悬停按的时间,这个值会在鼠标抬起是使用。
然后通过鼠标位置发出射线去拾取点击的地面位置,然后通过角色位置和拾取位置计算朝向,根据朝向去移动。

void ATopDownProjectPlayerController::OnSetDestinationTriggered()
{
	// 长按时将帧的时间存储在变量内,用于鼠标抬起时判断是否为点击事件
	FollowTime += GetWorld()->GetDeltaSeconds();
	
	// 用于存储点击位置的数据信息
	FHitResult Hit;
	bool bHitSuccessful = false; //是否成功获取点击位置信息
	//bIsTouch是由屏幕触摸回调触发,并在触摸回调内设置其开启关闭,因为鼠标点击和触摸点击逻辑一样,只是获取点击地面位置的函数不一样。
	if (bIsTouch)
	{
		bHitSuccessful = GetHitResultUnderFinger(ETouchIndex::Touch1, ECollisionChannel::ECC_Visibility, true, Hit);
	}
	else
	{
		bHitSuccessful = GetHitResultUnderCursor(ECollisionChannel::ECC_Visibility, true, Hit);
	}

	// 拾取到为止,从Hit内获取点击位置
	if (bHitSuccessful)
	{
		CachedDestination = Hit.Location;
	}
	
	// 通过点击位置和角色位置计算出角色移动方向,并调用AddMovementInput移动。
	APawn* ControlledPawn = GetPawn();
	if (ControlledPawn != nullptr)
	{
		FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
		ControlledPawn->AddMovementInput(WorldDirection, 1.0, false);
	}
}

AddMovementInput有三个值:

  • 方向向量(通常是一个FVector):这个参数定义了移动的方向。它可以是代表前后左右移动的二维向量,也可以是包含垂直移动的三维向量。
  • 缩放值(通常是一个浮点数float):这个参数用于调整移动的速度或幅度。通过改变这个值,你可以控制角色移动的快慢。
  • 强制标志(通常是一个布尔值bool):这个参数用于指定是否强制添加移动输入,即使某些条件不满足。它允许开发者在特定情况下覆盖正常的移动逻辑。
ControlledPawn->AddMovementInput(WorldDirection, 1.0, false);

最后我们看一下鼠标抬起事件,鼠标抬起事件首先判断鼠标悬停的时间,如果时间小于我们设置的ShortPressThreshold值,则代表当前属于点击事件,则可以触发自动寻路和播放粒子特效。如果时间大于,则代表属于鼠标悬停,角色跟随鼠标事件,不会触发自动寻路。在最后将FollowTime 设置为零。

void ATopDownProjectPlayerController::OnSetDestinationReleased()
{
	// 判断是否触发的点击事件,
	if (FollowTime <= ShortPressThreshold)
	{
		// 使用函数库,将角色移动到目标位置
		UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, CachedDestination);
		//使用Niagara生成生成粒子特效
		UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, FXCursor, CachedDestination, FRotator::ZeroRotator, FVector(1.f, 1.f, 1.f), true, true, ENCPoolMethod::None, true);
	}

	FollowTime = 0.f;
}

还有两个是触摸屏触发的事件,它内部只是设置bIsTouch布尔值,这个值为true代表是触摸屏触发的此事件,所以可以看到在悬停时将其设置为true,然后触发悬停事件,然后在抬起时将其设置为false。这样即使你这次是触摸屏触发的事件,在下一次修改为鼠标也是没问题的。

void ATopDownProjectPlayerController::OnTouchTriggered()
{
	bIsTouch = true;
	OnSetDestinationTriggered();
}

void ATopDownProjectPlayerController::OnTouchReleased()
{
	bIsTouch = false;
	OnSetDestinationReleased();
}

弊端

由于此案例是一个教学案例,所以代码很精简,只实现了简单的功能,其也只适合制作单机游戏,如果将其设置为包含服务器的客户端运行的话,会发现它的自动寻路功能将会失效,那么我们还需要使用其它的方式实现。

UE5俯视角游戏案例代码查看_服务器_06


接下来,我将在UE5 RPG的文章中实现可以在服务器上运行的自动寻路功能。