UE4 C++實現簡單的AI及遇到的坑
來自專欄 UE4雜貨鋪
我們這篇文章的宗旨是用代碼實現簡單的AI,有多簡單,只是搭建一個具有PawnSensing組件和回調的AI。重點也不是做一個AI,而是了解一下C++代碼如何搭建最基本的AI(關於藍圖如何創建請自行百度)。
如果英語還不錯的話,可以移步 參考鏈接,鏈接中的內容除了創建基本的PawnSensing組件還有關於行為樹的東西
雖然一直都聽到這樣的聲音「市面上關於UE4 C++的教程太少啦,都是一些非常基礎的,想學點稍微深入的都沒有」,但是我覺得有這些基礎就夠啦,剩下的就是學習方法啦。首先可以自己設計一些簡單的需求,用純代碼的方式去實現,實現的過程中你會發現你哪裡不會,再去Google(英語不好,必應也可以,不太推薦百度,原因大家都懂)就好啦。其次你已經回了藍圖啦,為啥不試著將藍圖能實現的功能轉化為用代碼去實現呢,這部也是一種學習途徑嗎。
廢話到此為止,接下來開始正文。
一、添加模塊
我們首先創建一個第三人稱C++模板。因為AI是單獨的模塊,所以我們需要修改以來關係,找到.Build.cs,在PublicDependency中添加 AIModule,改完之後最好重啟一下VS和UnrealEditor。
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "AIModule" });
二、創建AICharacter
我們基於ACharacter創建我們的AI,此處我們命名為AMyAICharacter,首先使用前置聲明定義PawnSensing組件,並定義對應的回調函數(此處必須使用UFUNCTION宏,否則會綁定失敗),核心代碼如下(想翻閱完整代碼,請翻至文章末尾處)
// 定義PawnSensing組件 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mantra") class UPawnSensingComponent* PawnSensingComp; // 定義SeePawn的回調 UFUNCTION() void OnMySeePawn(APawn* Pawn); // 定義HearPawn的回調 UFUNCTION() void OnHeardNoise(APawn* NoiseInstigator, const FVector& Location, float Volume);
在.cpp文件中初始化組件並綁定回調函數,此處我們在回調函數中只是做了簡單的列印處理,並沒有書寫任何邏輯。下面為完整的cpp文件
#include "MyAICharacter.h"#include "Perception/PawnSensingComponent.h"// Sets default valuesAMyAICharacter::AMyAICharacter(){ // Set this character to call Tick() every frame. You can turn this off to improve performance if you dont need it. PrimaryActorTick.bCanEverTick = true; // 創建組件,注意導入頭文件 Perception/PawnSensingComponent PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("PawnSensingComp")); PawnSensingComp->OnSeePawn.AddDynamic(this, &AMyAICharacter::OnMySeePawn);}// Called when the game starts or when spawnedvoid AMyAICharacter::BeginPlay(){ Super::BeginPlay(); // 該事件如果放到構造函數中綁定就不會執行 PawnSensingComp->OnHearNoise.AddDynamic(this, &AMyAICharacter::OnHeardNoise);}// Called every framevoid AMyAICharacter::Tick(float DeltaTime){ Super::Tick(DeltaTime);}void AMyAICharacter::OnMySeePawn(APawn * Pawn){ UE_LOG(LogTemp, Warning, TEXT("Has Seen Pawn: "), *(Pawn->GetName()));}void AMyAICharacter::OnHeardNoise(APawn * NoiseInstigator, const FVector & Location, float Volume){ UE_LOG(LogTemp, Warning, TEXT("Hear %s fired noise at location: %s"), *(NoiseInstigator->GetName()), *(Location.ToString()));}
上述注釋中也寫的比較清楚,遇到了一個神奇的坑:
如果我們把OnHearNoise的綁定寫到構造函數中則不會正常回調,就好像沒有綁定一樣,但是同樣的代碼寫到BeginPlay中就正常調用。
我用的是編輯器版本如下,不確定是不是這個版本的bug?還是說這個回調需要特殊處理?反正調試這個bug花了很久,個人感覺這也是這篇文章中最有價值的部分。
三、為PlayerCharacter添加「發聲」功能
如果Pawn想發出聲音,那麼必須要添加一個UPawnNoiseEmitterComponent,之後調用MakeNoise才有效。
所以我們首先在玩家代碼中(MyProjectCharacter)定義這樣一個組件變數
UPROPERTY() class UPawnNoiseEmitterComponent* NoiseEmitter;
之後再.cpp文件中創建並在合適的地方調用MakeNoise(此處我們在玩家跳躍的方法中調用MakeNoise,但是因為默認的模板是調用ACharacter類中的Jump,所以我們在我們的類中重寫了這個方法)
四、測試
基於MyAICharacter創建一個藍圖類並給定Mesh,適當調整PawnSensing的範圍,然後拖放到場景中。結果與我們預期的一樣。只不過官方會做一些優化處理,如果玩家處於可見範圍內,則不會監聽聽覺。
完整代碼如下
MyAICharacter.h
#pragma once#include "CoreMinimal.h"#include "GameFramework/Character.h"#include "MyAICharacter.generated.h"UCLASS()class MYPROJECT_API AMyAICharacter : public ACharacter{ GENERATED_BODY()public: // Sets default values for this characters properties AMyAICharacter();protected: // Called when the game starts or when spawned virtual void BeginPlay() override;public: // Called every frame virtual void Tick(float DeltaTime) override; // 定義PawnSensing組件 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mantra") class UPawnSensingComponent* PawnSensingComp; // 定義SeePawn的回調 UFUNCTION() void OnMySeePawn(APawn* Pawn); // 定義HearPawn的回調 UFUNCTION() void OnHeardNoise(APawn* NoiseInstigator, const FVector& Location, float Volume); };
MyAICharacter.cpp
// Fill out your copyright notice in the Description page of Project Settings.#include "MyAICharacter.h"#include "Perception/PawnSensingComponent.h"// Sets default valuesAMyAICharacter::AMyAICharacter(){ // Set this character to call Tick() every frame. You can turn this off to improve performance if you dont need it. PrimaryActorTick.bCanEverTick = true; // 創建組件,注意導入頭文件 Perception/PawnSensingComponent PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("PawnSensingComp")); PawnSensingComp->OnSeePawn.AddDynamic(this, &AMyAICharacter::OnMySeePawn);}// Called when the game starts or when spawnedvoid AMyAICharacter::BeginPlay(){ Super::BeginPlay(); // 該事件如果放到構造函數中綁定就不會執行 PawnSensingComp->OnHearNoise.AddDynamic(this, &AMyAICharacter::OnHeardNoise);}// Called every framevoid AMyAICharacter::Tick(float DeltaTime){ Super::Tick(DeltaTime);}void AMyAICharacter::OnMySeePawn(APawn * Pawn){ UE_LOG(LogTemp, Warning, TEXT("Has Seen Pawn: "), *(Pawn->GetName()));}void AMyAICharacter::OnHeardNoise(APawn * NoiseInstigator, const FVector & Location, float Volume){ UE_LOG(LogTemp, Warning, TEXT("Hear %s fired noise at location: %s"), *(NoiseInstigator->GetName()), *(Location.ToString()));}
MyProjectCharacter.h
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.#pragma once#include "CoreMinimal.h"#include "GameFramework/Character.h"#include "MyProjectCharacter.generated.h"UCLASS(config=Game)class AMyProjectCharacter : public ACharacter{ GENERATED_BODY() /** Camera boom positioning the camera behind the character */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true")) class USpringArmComponent* CameraBoom; /** Follow camera */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true")) class UCameraComponent* FollowCamera;public: AMyProjectCharacter(); /** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera) float BaseTurnRate; /** Base look up/down rate, in deg/sec. Other scaling may affect final rate. */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera) float BaseLookUpRate;protected: /** Resets HMD orientation in VR. */ void OnResetVR(); /** Called for forwards/backward input */ void MoveForward(float Value); /** Called for side to side input */ void MoveRight(float Value); /** * Called via input to turn at a given rate. * @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired turn rate */ void TurnAtRate(float Rate); /** * Called via input to turn look up/down at a given rate. * @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired turn rate */ void LookUpAtRate(float Rate); /** Handler for when a touch input begins. */ void TouchStarted(ETouchIndex::Type FingerIndex, FVector Location); /** Handler for when a touch input stops. */ void TouchStopped(ETouchIndex::Type FingerIndex, FVector Location);protected: // APawn interface virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; // End of APawn interfacepublic: /** Returns CameraBoom subobject **/ FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; } /** Returns FollowCamera subobject **/ FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; } virtual void Jump() override; UPROPERTY() class UPawnNoiseEmitterComponent* NoiseEmitter;};
MyProjectCharacter.cpp
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.#include "MyProjectCharacter.h"#include "HeadMountedDisplayFunctionLibrary.h"#include "Camera/CameraComponent.h"#include "Components/CapsuleComponent.h"#include "Components/InputComponent.h"#include "GameFramework/CharacterMovementComponent.h"#include "GameFramework/Controller.h"#include "GameFramework/SpringArmComponent.h"#include "Components/PawnNoiseEmitterComponent.h"//////////////////////////////////////////////////////////////////////////// AMyProjectCharacterAMyProjectCharacter::AMyProjectCharacter(){ // Set size for collision capsule GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f); // set our turn rates for input BaseTurnRate = 45.f; BaseLookUpRate = 45.f; // Dont rotate when the controller rotates. Let that just affect the camera. bUseControllerRotationPitch = false; bUseControllerRotationYaw = false; bUseControllerRotationRoll = false; // Configure character movement GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input... GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f); // ...at this rotation rate GetCharacterMovement()->JumpZVelocity = 600.f; GetCharacterMovement()->AirControl = 0.2f; // Create a camera boom (pulls in towards the player if there is a collision) CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom")); CameraBoom->SetupAttachment(RootComponent); CameraBoom->TargetArmLength = 300.0f; // The camera follows at this distance behind the character CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller // Create a follow camera FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera")); FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm // Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character) // are set in the derived blueprint asset named MyCharacter (to avoid direct content references in C++) // 創建組件,這個組件必須存在,否則不會發出噪音 NoiseEmitter = CreateDefaultSubobject<UPawnNoiseEmitterComponent>(TEXT("NoiseEmitter"));}//////////////////////////////////////////////////////////////////////////// Inputvoid AMyProjectCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent){ // Set up gameplay key bindings check(PlayerInputComponent); PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMyProjectCharacter::Jump); PlayerInputComponent->BindAction("Jump", IE_Released, this, &AMyProjectCharacter::StopJumping); PlayerInputComponent->BindAxis("MoveForward", this, &AMyProjectCharacter::MoveForward); PlayerInputComponent->BindAxis("MoveRight", this, &AMyProjectCharacter::MoveRight); // We have 2 versions of the rotation bindings to handle different kinds of devices differently // "turn" handles devices that provide an absolute delta, such as a mouse. // "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput); PlayerInputComponent->BindAxis("TurnRate", this, &AMyProjectCharacter::TurnAtRate); PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput); PlayerInputComponent->BindAxis("LookUpRate", this, &AMyProjectCharacter::LookUpAtRate); // handle touch devices PlayerInputComponent->BindTouch(IE_Pressed, this, &AMyProjectCharacter::TouchStarted); PlayerInputComponent->BindTouch(IE_Released, this, &AMyProjectCharacter::TouchStopped); // VR headset functionality PlayerInputComponent->BindAction("ResetVR", IE_Pressed, this, &AMyProjectCharacter::OnResetVR);}void AMyProjectCharacter::Jump(){ Super::Jump(); UE_LOG(LogTemp, Warning, TEXT("Self Jump")); // 發射噪音信號 MakeNoise(1, this, GetActorLocation());}void AMyProjectCharacter::OnResetVR(){ UHeadMountedDisplayFunctionLibrary::ResetOrientationAndPosition();}void AMyProjectCharacter::TouchStarted(ETouchIndex::Type FingerIndex, FVector Location){ Jump();}void AMyProjectCharacter::TouchStopped(ETouchIndex::Type FingerIndex, FVector Location){ StopJumping();}void AMyProjectCharacter::TurnAtRate(float Rate){ // calculate delta for this frame from the rate information AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds());}void AMyProjectCharacter::LookUpAtRate(float Rate){ // calculate delta for this frame from the rate information AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds());}void AMyProjectCharacter::MoveForward(float Value){ if ((Controller != NULL) && (Value != 0.0f)) { // find out which way is forward const FRotator Rotation = Controller->GetControlRotation(); const FRotator YawRotation(0, Rotation.Yaw, 0); // get forward vector const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); AddMovementInput(Direction, Value); }}void AMyProjectCharacter::MoveRight(float Value){ if ( (Controller != NULL) && (Value != 0.0f) ) { // find out which way is right const FRotator Rotation = Controller->GetControlRotation(); const FRotator YawRotation(0, Rotation.Yaw, 0); // get right vector const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y); // add movement in that direction AddMovementInput(Direction, Value); }}
歡迎大家加群討論:虛幻社區 524418526
推薦閱讀:
※《Exploring in UE4》物理模塊淺析[原理分析]
※從零開始手敲次世代遊戲引擎(四十九)
※虛幻4多pass渲染向虛幻4巨佬低頭【第一卷:Dynamic Character HitMask】
※pixi-action - 一個類似 cocos2d-x 使用方法的 Pixi.js 動畫插件
※[GDC15]Parallelizing the Naughty Dog Engine using Fibers