UE4学习笔记(4)官方教程代码

发表于2018-01-15
评论0 3.9k浏览

本篇文章主要是通过分析UE4虚幻引擎中的官方教程代码,来帮助大家去学习如何使用UE4做开发。

实现平台:win8.1  UE4.10

按教程练习。

1、实现Pawn移动(input)

MyPawn.h

// Fill out your copyright notice in the Description page of Project Settings.  
#pragma once  
#include "GameFramework/Pawn.h"  
#include "MyPawn.generated.h"  
UCLASS()  
class MYPROJECT2_BP_API AMyPawn : public APawn  
{  
    GENERATED_BODY()  
public:  
    // Sets default values for this pawn's properties  
    AMyPawn();  
    // Called when the game starts or when spawned  
    virtual void BeginPlay() override;  
    // Called every frame  
    virtual void Tick( float DeltaSeconds ) override;  
    // Called to bind functionality to input  
    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;  
    UPROPERTY(EditAnywhere)  
        USceneComponent* OurVisibleComponent;  
    void MoveForward(float Value);  
    void MoveRight(float Value);  
    void Bigger();  
    void Smaller();  
    bool bGrowing;  
    FVector CurrentVelocity;  
};  
Mypawn.cpp
// Fill out your copyright notice in the Description page of Project Settings.  
#include "MyProject2_BP.h"  
#include "MyPawn.h"  
// Sets default values  
AMyPawn::AMyPawn()  
{  
    // 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;  
    AutoPossessPlayer = EAutoReceiveInput::Player0;  
    RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("MyRootComponent"));  
    UCameraComponent* Mycamera = CreateDefaultSubobject<UCameraComponent>(TEXT("OurCamera"));  
    OurVisibleComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("OurVisibleComponent"));  
    Mycamera->AttachTo(RootComponent);  
    Mycamera->SetRelativeLocation(FVector(-250.0f, 0.0f, 250.0f));  
    Mycamera->SetRelativeRotation(FRotator(-45.0f, 0, 0));  
    OurVisibleComponent->AttachTo(RootComponent);  
}  
// Called when the game starts or when spawned  
void AMyPawn::BeginPlay()  
{  
    Super::BeginPlay();  
}  
// Called every frame  
void AMyPawn::Tick( float DeltaTime )  
{  
    Super::Tick( DeltaTime );  
    float CurrentScale = OurVisibleComponent->GetComponentScale().X;  
    if (bGrowing)  
    {  
        //  在一秒的时间内增长到两倍的大小  
        CurrentScale += DeltaTime;  
    }  
    else  
    {  
        // 随着增长收缩到一半  
        CurrentScale -= (DeltaTime * 0.5f);  
    }  
    // 确认永不低于起始大小,或增大之前的两倍大小。  
    CurrentScale = FMath::Clamp(CurrentScale, 1.0f, 2.0f);  
    OurVisibleComponent->SetWorldScale3D(FVector(CurrentScale));  
// 基于"MoveX"和 "MoveY"坐标轴来处理移动  
    if (!CurrentVelocity.IsZero())  
    {  
        FVector NewLocation = GetActorLocation() + (CurrentVelocity * DeltaTime);  
        SetActorLocation(NewLocation);  
    }  
}  
// Called to bind functionality to input  
void AMyPawn::SetupPlayerInputComponent(class UInputComponent* InputComponent)  
{  
    Super::SetupPlayerInputComponent(InputComponent);  
    InputComponent->BindAction("Bigger", IE_Pressed, this, &AMyPawn::Bigger);  
    InputComponent->BindAction("Smaller", IE_Pressed, this, &AMyPawn::Smaller);  
    InputComponent->BindAxis("MoveForward", this, &AMyPawn::MoveForward);  
    InputComponent->BindAxis("MoveRight", this, &AMyPawn::MoveRight);  
}  
void AMyPawn::MoveForward(float Value)  
{  
    CurrentVelocity.X = Value * 100.0f;  
}  
void AMyPawn::MoveRight(float Value)  
{  
    CurrentVelocity.Y = Value * 100.0f;  
}  
void AMyPawn::Bigger()  
{  
    bGrowing = true;  
}  
void AMyPawn::Smaller()  
{  
    bGrowing = false;  
}  


2、实现相机自动切换(注意需要两个camera)

CameraDirector.h

// Fill out your copyright notice in the Description page of Project Settings.  
#pragma once  
#include "GameFramework/Actor.h"  
#include "CameraDirector.generated.h"  
UCLASS()  
class MYPROJECT2_BP_API ACameraDirector : public AActor  
{  
    GENERATED_BODY()  
public:   
    // Sets default values for this actor's properties  
    ACameraDirector();  
    // Called when the game starts or when spawned  
    virtual void BeginPlay() override;  
    // Called every frame  
    virtual void Tick( float DeltaSeconds ) override;  
    UPROPERTY(EditAnywhere)  
        AActor* CameraOne;  
    UPROPERTY(EditAnywhere)  
        AActor* CameraTwo;  
    float TimeToNextCameraChange;  
};  
CameraDirector.cpp
// Fill out your copyright notice in the Description page of Project Settings.  
#include "MyProject2_BP.h"  
#include "CameraDirector.h"  
#include "Kismet/GameplayStatics.h"  
// Sets default values  
ACameraDirector::ACameraDirector()  
{  
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.  
    PrimaryActorTick.bCanEverTick = true;  
}  
// Called when the game starts or when spawned  
void ACameraDirector::BeginPlay()  
{  
    Super::BeginPlay();  
}  
// Called every frame  
void ACameraDirector::Tick( float DeltaTime )  
{  
    Super::Tick( DeltaTime );  
    const float TimeBetweenCameraChanges = 2.0f;  
    const float SmoothBlendTime = 0.75;  
    TimeToNextCameraChange -= DeltaTime;  
    if (TimeToNextCameraChange<=0.0f)  
    {  
        TimeToNextCameraChange += TimeBetweenCameraChanges;  
        APlayerController* OurPlayerController = UGameplayStatics::GetPlayerController(this, 0);  
        if (OurPlayerController)  
        {  
            if ((OurPlayerController->GetViewTarget() != CameraOne) && (CameraOne != nullptr))  
            {  
                // 立即切换到相机1。  
                OurPlayerController->SetViewTarget(CameraOne);  
            }  
            else if ((OurPlayerController->GetViewTarget() != CameraTwo) && (CameraTwo != nullptr))  
            {  
                // 平滑地混合到相机2。  
                OurPlayerController->SetViewTargetWithBlend(CameraTwo, SmoothBlendTime);  
            }  
        }  
    }  
}  


3、实现倒计时(timer)(注意宏的用法、C++与bluepringt交互)

CountDown.h

// Fill out your copyright notice in the Description page of Project Settings.  
#pragma once  
#include "GameFramework/Actor.h"  
#include "CountDown.generated.h"  
UCLASS()  
class MYPROJECT2_BP_API ACountDown : public AActor  
{  
    GENERATED_BODY()  
public:   
    // Sets default values for this actor's properties  
    ACountDown();  
    // Called when the game starts or when spawned  
    virtual void BeginPlay() override;  
    // Called every frame  
    virtual void Tick( float DeltaSeconds ) override;  
    UPROPERTY(EditAnywhere, Category = "CountNumber")  
    int32 CountDownTime;  
    UTextRenderComponent* CountDownText;  
    void UpdataTimerDisplay();  
    void AdvanceTimer();  
    UFUNCTION(BlueprintNativeEvent, Category = "CountNumber")  
    void CountdownHasFinish();  
    virtual void CountdownHasFinish_Implementation();  
    FTimerHandle CountdownTimerhandle;  
};  

CountDown.cpp

// Fill out your copyright notice in the Description page of Project Settings.  
#include "MyProject2_BP.h"  
#include "CountDown.h"  
// Sets default values  
ACountDown::ACountDown()  
{  
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.  
    PrimaryActorTick.bCanEverTick = true;  
    CountDownText = CreateDefaultSubobject<UTextRenderComponent>(TEXT("CountdownNumber"));  
    CountDownText->SetHorizontalAlignment(EHTA_Center);  
    CountDownText->SetWorldSize(150.0f);  
    RootComponent = CountDownText;  
    CountDownTime = 3.0f;  
}  
void ACountDown::UpdataTimerDisplay()  
{  
    CountDownText->SetText((FString::FromInt(FMath::Max(CountDownTime, 0))));  
}  
void ACountDown::AdvanceTimer()  
{  
    --CountDownTime;  
    UpdataTimerDisplay();  
    if (CountDownTime<1)  
    {  
        GetWorldTimerManager().ClearTimer(CountdownTimerhandle);  
        CountdownHasFinish();  
    }  
}  
//void ACountDown::CountdownHasFinish()  
//{  
//  //CountDownText->SetText(TEXT("GO!"));  
//}  
void ACountDown::CountdownHasFinish_Implementation()  
{  
    //Change to a special readout  
    CountDownText->SetText(TEXT("GO!"));  
}  
// Called when the game starts or when spawned  
void ACountDown::BeginPlay()  
{  
    Super::BeginPlay();  
    UpdataTimerDisplay();  
    GetWorldTimerManager().SetTimer(CountdownTimerhandle, this, &ACountDown::AdvanceTimer, 1.0f, true);  
}  
// Called every frame  
void ACountDown::Tick( float DeltaTime )  
{  
    Super::Tick( DeltaTime );  
}  


4、Pwan与碰撞(粒子特效) 注意碰撞时,按下space才会产生火焰

ColisionPawn.h
// Fill out your copyright notice in the Description page of Project Settings.  
#pragma once  
#include "GameFramework/Pawn.h"  
#include "ColisionPawn.generated.h"  
UCLASS()  
class MYPROJECT2_BP_API AColisionPawn : public APawn  
{  
    GENERATED_BODY()  
public:  
    // Sets default values for this pawn's properties  
    AColisionPawn();  
    // Called when the game starts or when spawned  
    virtual void BeginPlay() override;  
    // Called every frame  
    virtual void Tick( float DeltaSeconds ) override;  
    // Called to bind functionality to input  
    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;  
    UParticleSystemComponent* MyParticleSystem;  
    class UColisionPawnMovementComponent* MyMovementComponent;  
    virtual UPawnMovementComponent* GetMovementComponent() const override;  
    void MoveForward(float AxisValue);  
    void MoveRight(float AxisValue);  
    void Turn(float AxisValue);  
    void ParticleToggle();  
};  

ColisionPawn.cpp
// Fill out your copyright notice in the Description page of Project Settings.  
#include "MyProject2_BP.h"  
#include "ColisionPawn.h"  
#include "ColisionPawnMovementComponent.h"  
// Sets default values  
AColisionPawn::AColisionPawn()  
{  
    // 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.f);  
    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->AttachTo(SphereComponent);  
    static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));  
    if (SphereVisualAsset.Succeeded())    
    {  
        SphereVisual->SetStaticMesh(SphereVisualAsset.Object);  
        SphereVisual->SetRelativeLocation(FVector(0, 0, 0));  
        SphereVisual->SetWorldScale3D(FVector(0.8f));  
    }  
    // Create a particle system that we can activate or deactivate  
    MyParticleSystem = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("MovementParticle"));  
    MyParticleSystem->AttachTo(SphereVisual);  
    MyParticleSystem->bAutoActivate = false;  
    MyParticleSystem->SetRelativeLocation(FVector(-20.0f, 0.0f, 20.0f));  
    static ConstructorHelpers::FObjectFinder<UParticleSystem> ParticleAsset(TEXT("/Game/StarterContent/Particles/P_Fire.P_Fire"));  
    if (ParticleAsset.Succeeded())  
    {  
        MyParticleSystem->SetTemplate(ParticleAsset.Object);  
    }  
    // Use a spring arm to give the camera smooth, natural-feeling motion.  
    USpringArmComponent* SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraAttachmentArm"));  
    SpringArm->AttachTo(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* MyCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("AcrualCamera"));  
    MyCamera->AttachTo(SpringArm, USpringArmComponent::SocketName);  
    // Take control of the default player  
    AutoPossessPlayer = EAutoReceiveInput::Player0;  
    // Create an instance of our movement component, and tell it to update the root.  
    MyMovementComponent = CreateDefaultSubobject<UColisionPawnMovementComponent>(TEXT("CustomMovementComponent"));  
    MyMovementComponent->UpdatedComponent = RootComponent;  
}  
UPawnMovementComponent* AColisionPawn::GetMovementComponent() const  
{  
    return MyMovementComponent;  
}  
void AColisionPawn::MoveForward(float AxisValue)  
{  
    if (MyMovementComponent && (MyMovementComponent->UpdatedComponent == RootComponent))  
    {  
        MyMovementComponent->AddInputVector(GetActorForwardVector() * AxisValue);  
    }  
}  
void AColisionPawn::MoveRight(float AxisValue)  
{  
    if (MyMovementComponent && (MyMovementComponent->UpdatedComponent == RootComponent))  
    {  
        MyMovementComponent->AddInputVector(GetActorRightVector() * AxisValue);  
    }  
}  
void AColisionPawn::Turn(float AxisValue)  
{  
    FRotator NewRotation = GetActorRotation();  
    NewRotation.Yaw += AxisValue;  
    SetActorRotation(NewRotation);  
}  
void AColisionPawn::ParticleToggle()  
{  
    if (MyParticleSystem && MyParticleSystem->Template)  
    {  
        MyParticleSystem->ToggleActive();  
    }  
}  
// Called when the game starts or when spawned  
void AColisionPawn::BeginPlay()  
{  
    Super::BeginPlay();  
}  
// Called every frame  
void AColisionPawn::Tick( float DeltaTime )  
{  
    Super::Tick( DeltaTime );  
}  
// Called to bind functionality to input  
void AColisionPawn::SetupPlayerInputComponent(class UInputComponent* InputComponent)  
{  
    Super::SetupPlayerInputComponent(InputComponent);  
    InputComponent->BindAction("ParticleToggle", IE_Pressed, this, &AColisionPawn::ParticleToggle);  
    InputComponent->BindAxis("MoveForward", this, &AColisionPawn::MoveForward);  
    InputComponent->BindAxis("MoveRight", this, &AColisionPawn::MoveRight);  
    InputComponent->BindAxis("Turn", this, &AColisionPawn::Turn);  
}

ColisionPawnMovementComponent.h

// Fill out your copyright notice in the Description page of Project Settings.  
#pragma once  
#include "GameFramework/PawnMovementComponent.h"  
#include "ColisionPawnMovementComponent.generated.h"  
/** 
 *  
 */  
UCLASS()  
class MYPROJECT2_BP_API UColisionPawnMovementComponent : public UPawnMovementComponent  
{  
    GENERATED_BODY()  
public:  
    virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction);  
};  
ColisionPawnMovementComponent.cpp
// Fill out your copyright notice in the Description page of Project Settings.  
#include "MyProject2_BP.h"  
#include "ColisionPawnMovementComponent.h"  
void UColisionPawnMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)  
{  
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);  
    // Make sure that everything is still valid, and that we are allowed to move.  
    if (!PawnOwner||!UpdatedComponent||ShouldSkipUpdate(DeltaTime))  
    {  
        return;  
    }  
    // Get (and then clear) the movement vector that we set in ACollidingPawn::Tick  
    FVector DesireMovementthisFrame = ConsumeInputVector().GetClampedToMaxSize(1.0f)*DeltaTime*150.0f;  
    if (!DesireMovementthisFrame.IsNearlyZero())  
    {  
        FHitResult Hit;  
        SafeMoveUpdatedComponent(DesireMovementthisFrame, UpdatedComponent->GetComponentRotation(), true, Hit);  
        // If we bumped into something, try to slide along it  
        if (Hit.IsValidBlockingHit())  
        {  
            SlideAlongSurface(DesireMovementthisFrame, 1.f - Hit.Time, Hit.Normal, Hit);  
        }  
    }  
}  

This TickComponent function makes use of a few of the powerful features offered by the UPawnMovementComponentclass. 

ConsumeInputVector reports and clears the value of a built-in variable that we will use to store our movement inputs. 

SafeMoveUpdatedComponent uses Unreal Engine physics to move our Pawn Movement Component while respecting solid barriers. 

SlideAlongSurface handles the calculations and physics involved with sliding smoothly along collision surfaces like walls and ramps when a move results in a collision, rather than simply stopping in place and sticking to the wall or ramp. 

There are more features included in Pawn Movement Components that are worthy of examination, but they are not needed for the scope of this tutorial. Looking at other classes, such as Floating Pawn MovementSpectator Pawn Movement, or Character Movement Component, could provide additional usage examples and ideas.


5、User Interface With UMG

Work-In-Progress Code

HowTo_UMG.Build.cs

// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class HowTo_UMG : ModuleRules
{
    public HowTo_UMG(TargetInfo Target)
    {
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG" });
        //PrivateDependencyModuleNames.AddRange(new string[] {  });
        // Uncomment if you are using Slate UI
        PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");
        // if ((Target.Platform == UnrealTargetPlatform.Win32) || (Target.Platform == UnrealTargetPlatform.Win64))
        // {
        //      if (UEBuildConfiguration.bCompileSteamOSS == true)
        //      {
        //          DynamicallyLoadedModuleNames.Add("OnlineSubsystemSteam");
        //      }
        // }
    }
}

2. Extend Our Game Mode

  1. The menus we create will be made from User Widgets. We'll need to write a function that creates and displays a new User Widget, and then call that function when the game starts. We'll also need to keep track of what we have created so that we can remove it later. Since each project already comes with a custom GameMode class, we can simply open ours, which is defined in HowTo_UMGGameMode.h. The following functions and properties will need to be added to the bottom of the class:

    public: /** Called when the game starts. */ virtual void BeginPlay() override; /** Remove the current menu widget and create a new one from the specified class, if provided. */ UFUNCTION(BlueprintCallable, Category = "UMG Game") void ChangeMenuWidget(TSubclassOf<UUserWidget> NewWidgetClass); protected: /** The widget class we will use as our menu when the game starts. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UMG Game") TSubclassOf<UUserWidget> StartingWidgetClass; /** The widget instance that we are using as our menu. */ UPROPERTY() UUserWidget* CurrentWidget;
  2. In order to use User Widgets in our code, and add the following line to the top of the #include section:

    #include "Blueprint/UserWidget.h"
  3. Moving to HowTo_UMGGameMode.cpp now, we need to fill out the bodies of the two functions we declared. We'll start with overridingBeginPlay():

    void AHowTo_UMGGameMode::BeginPlay()  Super::BeginPlay(); ChangeMenuWidget(StartingWidgetClass); 

    When overriding functions from a parent class (referenced by the word Super), as we do here with BeginPlay, it is often important to call the parent class' version of that function. Since our version of the function is only meant to add one step to the end of the existing procedure, we call Super::BeginPlay in the first line of the function.

  4. Next, still in HowTo_UMGGameMode.cpp, we need to define how we change between menus. We will need to remove whatever User Widget we have active from the viewport, if any. Then we can create and a new User Widget and add it to the viewport.

    void AHowTo_UMGGameMode::ChangeMenuWidget(TSubclassOf<UUserWidget> NewWidgetClass)  if (CurrentWidget != nullptr)  CurrentWidget->RemoveFromViewport(); CurrentWidget = nullptr;  if (NewWidgetClass != nullptr)  CurrentWidget = CreateWidget<UUserWidget>(GetWorld(), NewWidgetClass); if (CurrentWidget != nullptr)  CurrentWidget->AddToViewport();   

    This code will create instances of any Widgets we provide and put them on the screen. It also removes them, so that only one is active at a time, although Unreal Engine can handle displaying and interacting with many Widgets at once. We never need to destroy a Widget directly, because removing it from the viewport and clearing (or changing) all variables that reference it will cause it to be cleaned up by Unreal Engine's garbage collection system.


We have built the code framework to create and display menus, and remove them when they are no longer needed. We're ready to return to theUnreal Editor and design our menu assets!

3. Create Menu Widget Blueprints

  1. In Unreal Editor, we can press the Compile button to build our new code. This will allow us to use User Widgets as menus.

  2. We will now create the User Widgets that our GameMode will use as menus. This is done with the Add New button in the Content Browser. The Widget Blueprint class is found in the User Interface category. We need to create two of these, one named MainMenu and one named NewGameMenu. Our game will begin at the Main Menu, and will have an option to proceed to the New Game Menu.

  3. Double-clicking the MainMenu Widget we've just created will take us to the Blueprint Designer, where we can create our menu layout.

  4. Let's drag a Button and a Text from the Common section of the Palette Panel to the Graph. This Button will eventually be used to open the New Game Menu.

  5. The first step in making our layout look right is to adjust the location and size of our Button. We should make the following changes:

    • Set the size to 200x200.

    • Set the position to (200, 100).

    • Rename it NewGameButton, just to make it easier to recoginze when we hook up functionality to it later on.

  6. Since we're not drawing a custom set of images for the Button, we can label it by dragging and dropping the Text Block onto it and making the following changes:

    • Set the text to say New Game.

    • Change the Visibility to Hit Test Invisible. This will prevent the Text Block from intercepting mouse-clicks that are intended for the Button underneath.

    • Set the name to NewGameText. This is not needed, but it's a good habit.

  7. Next, we'll want to make a "Quit" feature with a second Button and Text Block. Set those up in the same way as the New Game Button and Text Block except the following changes.

    • Set the name of the Button to QuitButton

    • Set the position of the button to 600, 100

    • Set the name of the Text Block to QuitText

  8. After that, we can add Events to our Buttons so that we can run code when a Button is clicked. This is done by locating and pressing the + next to the appropriate Event name in the Details Panel. In this case, OnClicked is the event we want to use. Create this event for both the NewGameButton amd QuitButton Widgets.

    Designers can build functionality with Blueprint scripting here, or C++ programmers can connect nodes that call exposed functions.

  9. For the Event called OnClicked(NewGameButton), we'll want to:

    • Connect a ChangeMenuWidget node to use the function we added to our GameMode earlier.

    • Set the New Widget Class field on the ChangeMenuWidget node to the NewGameMenu asset.

  10. For the OnClicked(QuitButton) Event, we'll want to:

    • Connect a Quit Game node.


4. Configure Our Game Mode

With our main menu built, we can set up a GameMode asset that will load it as soon as the level starts.

  1. In the Content Browser, we will add a Blueprint Class based on our project's GameMode. This makes it possible to set the exposed variables on those two classes to whatever values we want. To do this:

    • Click the Add button in the Content Browser.

    • Pick HowTo_UMGGameMode as the parent class. It will be listed in the All Classes section.

    • Name the resulting Blueprint asset MenuGameMode.

  2. In order to see our mouse cursor in-game, we'll need to create a Blueprint of the PlayerController as we did with our GameMode.

    • Click the Add button in the Content Browser again.

    • Select Player Controller from the Common Classes section.

    • Name the Blueprint MenuPlayerController.

  3. Edit MenuPlayerController.

    • Check the Show Mouse Cursor box.

  4. Edit MenuGameMode.

    • The Starting Widget Class must be set to the MainMenu asset in order to bring up our menu when the game begins.

    • The Default Pawn Class should be set to Pawn instead of DefaultPawn so that our player will not be able to fly around while in the menu.

    • The Player Controller Class should be set to the MenuPlayerController asset we created so that the mouse cursor will be shown in-game.

  5. In order for our Blueprint to be used, we must return to the Level Editor window and change the World Settings for our current Level via the Settings button.

    It is also possible to set the default GameMode in the Project Settings menu, under the Maps and Modes section. If you use this method, all of your levels will default to the GameMode you choose, unless overridden individually. Which method you use depends on how you prefer to set up your project.

  6. The World Settings Panel will open up. By default, it will be docked with the Details Panel, but it can be moved elsewhere. We need to set the Game Mode Override field to our MenuGameMode asset.


Our custom GameMode asset is now in effect on our level, configured to load our Main Menu, and use our Player Controller that shows the mouse cursor. If we run the game now, our Quit button will work as expected, and our New Game button will take us to an empty menu screen. Our next step will be to set up the New Game menu.

5. Build A Second Menu

  1. In the Content Browser, find and open the NewGameMenu asset we created earlier. This menu will contain a name-entry Text Box, aButton to play the game that cannot be pressed until a name is entered, and a Button to return to the main menu.

  2. To create the name entry box, we'll drag a Text Box (not a Text Block) into the layout.

  3. The Text Box should be configured with the following values:

    • Change the name to NameTextEntry

    • Position is (325, 200). This leaves room for a Text Block placed to the left of the Text Box.

    • Size is 250x40.

    • Font Size (under the "Style" heading) is 20.

  4. We can create the Play Game Button with a Text Block label the same way we created the Buttons on the previous menu.

    • For the Button: Change the name to PlayGameButton, Position to 200, 300, Size to 200, 100

    • For the Text Block: Change the name to PlayGameText, set Visibility to Hit Test Visible, and drag it on top of the PlayGameButton

  5. The Play Game Button will have a special feature - it will be enabled only when the name entered in the Text Box is not empty. We can use Unreal Motion Graphics' (UMG) bind feature to create a new function for the Is Enabled field (under the Behavior section).

    If there were complex rules for determining what constitutes a valid player name in our game, or if we needed to save the name to a C++ variable, we might expose a UFUNCTION on our GameMode or as a static function somewhere within our project. However, since we only care that the name string is not empty, we can script it right here in our Widget.

  6. To ensure that the Button is enabled if and only if the Text Box is not empty, we can convert the text from the Text Box to a string and then check that its length is greater than zero. Here is how that logic would appear:

  7. Let's add one more Button so we can back out and get to our Main Menu from here. This will be just like the Play Game Button from our Main Menu, but it will be positioned relative to the lower-right corner instead of the upper-left. To accomplish this, click the Anchorsdropdown in the Details Panel for the Button and find the appropriate graphical representation in the pop-up menu.

    • Change the name to MainMenuButton

    • Set the Position to -400, -200

    • Set the Size to 200x100

    Positioning our anchor at the lower-right corner does not change how size and position values work, so we'll need to make our position values negative in order to be on the screen. Size values will remain positive.

  8. We will now add scripting to our new Buttons by once again adding OnClicked events. The Main Menu Button will simply reload the Main Menu Widget, while the Play Game Button will deactivate our menu entirely by having no new Widget provided in the call to ourChangeMenuWidget function. This is shown by the phrase Select Class being displayed instead of the name of an actual class or asset.

    After deactivating the menu with the Play Game Button, we will be unable to do anything further in our game. This is the point at which we would normally load the first level, play an introductory cutscene, or spawn and possess a Pawn.

  9. We should now have two screens that look roughly as follows:

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引