• 虚幻4中有一些按键和快捷键很常用,牢记它们并运动到实际的项目开发中,将会大大地提高你的工作效率和使得工作更简便快捷。下面将列举它们出来: 按键 动作 鼠标左键 选择actor 鼠标...

    195828yo4fo0j4svzbsmo8

    虚幻4中有一些按键和快捷键很常用,牢记它们并运动到实际的项目开发中,将会大大地提高你的工作效率和使得工作更简便快捷。下面将列举它们出来:

    按键 动作
    鼠标左键 选择actor
    鼠标左键+拖动 前后移动和左右旋转摄像头
    鼠标右键 选择actor并打开右键菜单
    鼠标右键+拖动 旋转摄像头方向
    鼠标左键+鼠标右键+拖动 摄像头上下左右移动
    鼠标中键+拖动 摄像头上下左右移动
    滑轮向上 摄像机向前移动
    滑轮向下 摄像机向后移动
    F 聚焦选中的actor
    箭头方向键 摄像机前后左右移动
    W 选中平移工具
    E 选中旋转工具
    R 选中缩放工具
    W+任何鼠标按键 摄像机向前移动
    S+任何鼠标按键 摄像机向后移动
    A+任何鼠标按键 摄像机向左移动
    D+任何鼠标按键 摄像机向右移动
    E+任何鼠标按键 摄相机向上移动
    Q+任何鼠标按键 摄像机向下移动
    Z+任何鼠标按键 增加视野(鼠标释放后会恢复原状)
    C+任何鼠标按键 缩小视野(鼠标释放后会恢复原状)
    Ctrl+S 保存场景
    Ctrl+N 创建新场景
    Ctrl+O 打开一个已有的场景
    Ctrl+Alt+S 另存为新场景
    Alt+鼠标左键+拖动 复制当前选中的actor
    Alt+鼠标右键+拖动 摄像机前后移动
    Alt+P 进入Play预览模式
    Esc 退出预览模式
    F11 进入仿真模式


    转自:http://www.52vr.com/article-557-1.html

    展开全文
  • 学习心得,做下笔记,防止忘~ ...4.把“”“视口”右下角的input 中的Auto Receive input 设置为“”“player 0”,如下图: 5.在“”“事件图表”中添加变量 6.绘制蓝图关系 创建成功关系图后一定要

    学习心得,做下笔记,防止忘~

    1.新建关卡(level)

    2.新建蓝图类


    3.打开蓝图类

    视口——>rendering


    出现龙头,如下图,很好!


    4.把“”“视口”右下角的input 中的Auto Receive input 设置为“”“player 0”,如下图:


    5.在“”“事件图表”中添加变量


    6.绘制蓝图关系

    创建成功关系图后一定要编译


    7.设置循环1-10次,如下图:


    end~运行效果图:

    按“”“F”键盘上的按键,就可在看见效果


    展开全文
  • Unreal Engine 4 书籍翻译 Building an RPG with Unreal 三 第3章 探索和战斗 创建角色 接口 PlayerController The Pawn 游戏模式类 Unreal Engine 4 书籍翻译 Building an RPG with Unreal (三)好记性不如...

    Unreal Engine 4 书籍翻译 Building an RPG with Unreal (三)

    好记性不如烂笔头啊,还是记录一下!
    自己翻译的书,可能翻译的不好,大家见谅。
    欢迎大家指出翻译错误的地方以便修正


    第3章 探索和战斗

    我们完成一个我们游戏的设计和我们游戏的一个虚幻项目设置,现在是时候编写我们实际的游戏代码了。
    在这一章,我们会定义我们的游戏数据来创造一个可以在世界中移动的角色和游戏的基础战斗原型。在这一章我们将进行下列主题:

    • 创建玩家
    • 定义所有类、角色、敌人
    • 持续追踪活动的伙伴成员
    • 创建一个基本的回合制战斗引擎
    • 在屏幕上触发一个游戏

    创建角色

    我们要做的首要事情是创建一个新的Pawn类。在虚幻引擎中,Pawn是一个角色的表现形式,他是一个可以处理运动,物理和渲染的角色。
    现在如何让我们的Pawn角色去工作。一个玩家分为两个部分:
       1.Pawn——他负责处理责处理运动,物理,和渲染。
       2.Player Controller——是负责将玩家的输入进行处理,可以使得Pawn像玩家所想的那样行动。
    此外,我们执行一层分离,使Pwan类实现一个名为IControllableCharacter的接口。这不是绝对必要的,但确实有助于防止不同类需要太多了解对方(例如,任何其他的Actor可以实现IControllableCharacter接口,我们的玩家也可以同样的控制那些Actor)


    接口

    所以首先,我们会研究这个接口。我们从创建一个Actor的派生类开始,正如我们在前一章做。命名为 ControllableCharacter。虚幻引擎生成代码文件后,打开 ControllableCharacter.h,并改变它
    为下面的代码︰

    • ControllableCharacter.h
    
    #pragma once
    #include "Object.h"
    #include "ControllableCharacter.generated.h"
    
    UINTERFACE()
    class RPG_API UControllableCharacter : public UInterface
    {
        GENERATED_UINTERFACE_BODY()
    };
    
    class RPG_API IControllableCharacter
    {
        GENERATED_IINTERFACE_BODY()
        virtual void MoveVertical( float Value );
        virtual void MoveHorizontal( float Value );
    };
    

    让我们逐句分析下这些代码都做了什么?
    在虚幻中,接口有两个部分︰UInterface类和实际类。这两个类,结合虚幻的宏系统,允许您提供接口类转换宏 (这我们稍后将讨论)去转换到Actor实现的接口。

    • UInterface类有U前缀(所以在这种情况下,它是UControllableCharacter)只包含一行GENERATED_UINTERFACE_BODY()
    • 实际的接口类具有I前缀(所以在这种情况下,它是IControllableCharacter),其中有一行GENERATED_IINTERFACE_BODY(),还有实际定义的接口(在这里,我们定义的MoveVertical和MoveHorizontal的方法)。

    接下来,打开ControllableCharacter.cpp,将其更改为下面的代码

    • ControllableCharacter.cpp
    
    #include "RPG.h"
    #include "ControllableCharacter.h"
    UControllableCharacter::UControllableCharacter( const class FObjectInitializer& ObjectInitializer )
      : Super( ObjectInitializer )
    {
    }
    
    void IControllableCharacter::MoveVertical( float Value )
    {
    }
    
    void IControllableCharacter::MoveHorizontal( float Value )
    {
    }
    

    在这里,我们只定义了 UControllableCharacter 类的构造函数和 MoveHorizontal 和 MoveVertical 的默认实现。


    PlayerController

    接下来,我们要创建PlayerController。PlayerController的作用如前所述,是将玩家输入进行转换,从而实际控制角色的行动。
    创建一个新类,选择PlayerController作为基类。它的名字RPGPlayerController。
    打开生成的RPGPlayerController.h文件并在类中添加以下代码:

    • RPGPlayerController.h
    
    protected:
        void MoveVertical( float Value );
        void MoveHorizontal( float Value );
        virtual void SetupInputComponent() override;
    

    前两种方法MoveVertical和MoveHorizontal我们已经定义了,是我们将用来侦听玩家输入的两个方法。稍后我们将建立当玩家按下动作键或摇杆时调用这两个方法。
    最后一个方法SetupInputComponent,是一个重写的内置方法。顾名思义我们会在这方法中设置输入组件。
    接下来,打开RPGPlayerController.cpp并添加以下代码︰

    • RPGPlayerController.cpp
    
    void ARPGPlayerController::MoveVertical( float Value )
    {
        IControllableCharacter* pawn = Cast<IControllableCharacter>( GetPawn() );
        if( pawn != NULL )
        {
            pawn->MoveVertical( Value );
        }
    }
    void ARPGPlayerController::MoveHorizontal( float Value )
    {
        IControllableCharacter* pawn = Cast<IControllableCharacter>( GetPawn() );
        if( pawn != NULL )
        {
            pawn->MoveHorizontal( Value );
        }
    }
    void ARPGPlayerController::SetupInputComponent()
    {
        if( InputComponent == NULL )
        {
            InputComponent = ConstructObject<UInputComponent>(UInputComponent::StaticClass(), this, TEXT( "PC_InputComponent0" ) );
            InputComponent->RegisterComponent();
        }
        InputComponent->BindAxis( "MoveVertical", this, &ARPGPlayerController::MoveVertical );
        InputComponent->BindAxis( "MoveHorizontal", this, &ARPGPlayerController::MoveHorizontal );
        this->bShowMouseCursor = true;
    }
    

    在这里,我们实现在头文件中定义的方法。让我们来看看它们都做了些什么?
    MoveHorizontal和MoveVertical这两个方法是几乎完全相同,所以我们只需要要看看 MoveHorizontal。
    首先,我们使用下面的行︰

    
    IControllableCharacter* pawn = Cast<IControllableCharacter>( GetPawn() );
    

    这个方法获取一个PlayerController是当前正在控制中的Pawn指针,然后投射到我们定义Pawn的IControllableCharacter里的接口。
    接下来,我们检查指针是否为null,如果不空,我们就调用MoveHorizontal方法来使用A键和D键(如果是MoveVertical则使用W键和S键)控制Pawn,取值范围从-1到1(例如,在使用MoveHorizontal方法时,如果玩家按下A键,则值将会为-1。如果玩家按下D键,则值将会为1。如果玩家不按下任何按键,则值将会为0)
    在SetupInputComponent这个方法中,我们首先看看下面的代码:

    
    if( InputComponent == NULL )
    {
        InputComponent = ConstructObject<UInputComponent>(UInputComponent::StaticClass(), this, TEXT( "PC_InputComponent0" ) );
    }
    

    基本上,如果这里没有附加任何输入控件,我们构造一个新的UInputComponent类的实例(通过ConstructObject宏,我们传入的参数分别是类型,构造的哪个类,这个组件要附加到的Actor,新组件的名称)
    现在,我们有了一个输入组件,我们用它绑定我们的运动轴:

    
    InputComponent->BindAxis( "MoveVertical", this, &ARPGPlayerController::MoveVertical );
    

    BindAxis方法设置了一个函数引用一遍在使用输入轴的值时来调用。在前面的代码行,我们调用BindAxis传递的参数为轴的名称,一个指向调用函数Actor的指针,一个Actor用来处理输入的方法的引用
    最后,我们设置bShowMouseCursor为true,所以那虚幻不会隐藏鼠标光标。


    The Pawn

    现在现在,让我们来创建实际的Pawn。
    创建一个新类并选择Character作为他的父类。取名为RPGCharacter,打开RPGCharacter.h,并在类定义中更改代码为下面的代码:

    • RPGCharacter.h
    
    UCLASS()
    class RPG_API ARPGCharacter : public ACharacter, public IControllableCharacter
    {
    
        GENERATED_BODY()
        ARPGCharacter( const class FObjectInitializer& ObjectInitializer );
    
    public:
    
        virtual void MoveVertical( float Value );
        virtual void MoveHorizontal( float Value );
    };
    

    首先,我们用我们的新类实现IControllableCharacter里的接口。在类中,我们也定义了构造函数、MoveVertical和MoveHorizontal的方法(这是必须要实现的IControllableCharacter的接口)。
    接下来,打开RPGCharacter.cpp并添加以下代码︰

    • RPGCharacter.cpp
    
    ARPGCharacter::ARPGCharacter( const class FObjectInitializer &ObjectInitializer )
        : Super( ObjectInitializer )
    {
        bUseControllerRotationYaw = false;
        GetCharacterMovement()->bOrientRotationToMovement = true;
        GetCharacterMovement()->RotationRate = FRotator( 0.0f, 0.0f, 540.0f );
        GetCharacterMovement()->MaxWalkSpeed = 400.0f;
    }
    
    void ARPGCharacter::MoveVertical( float Value )
    {
        if( Controller != NULL && Value != 0.0f )
        {
            const FVector moveDir = FVector( 1, 0, 0 );
            AddMovementInput( moveDir, Value );
        }
    }
    
    void ARPGCharacter::MoveHorizontal( float Value )
    {
        if( Controller != NULL && Value != 0.0f )
        {
            const FVector moveDir = FVector( 0, 1, 0 );
            AddMovementInput( moveDir, Value );
        }
    }
    

    虚幻中的Character有一些内置的运动属性。在构造函数中,我们设置运动组件的一些默认值(默认情况下,Character旋转到面向运动方向的速度为540单位/秒,最大运动速度为400单位/秒)
    我们还用构造一个运动向量传递给AddMovementInput,来实现MoveHorizo​​ntal和MoveVertical方法。


    游戏模式类

    现在,为了使用这些类,我们需要建立了一类新的游戏模式。游戏模式可以指定默认使用的Pwan和PlayerController,我们还可以使用游戏模式的蓝图来修改这些默认的Pawn和PlayerController。
    创建一个新类,选择GameMode作为新类的父类。并将类命名为RPGGameMode。
    打开RPGGameMode.h并更改类的定义,使用以下代码︰

    • RPGGameMode.h
    
    UCLASS()
    class RPG_API ARPGGameMode : public AGameMode
    {
        GENERATED_BODY()
        ARPGGameMode( const class FObjectInitializer& ObjectInitializer );
    };
    

    正如我们前面已经做过的,我们只需要定义的CPP文件中的构造函数来实现。
    我们现在就在RPGGameMode.cpp中实现构造函数:

    • RPGGameMode.cpp
    
    #include "RPGPlayerController.h"
    #include "RPGCharacter.h"
    
    ARPGGameMode::ARPGGameMode( const class FObjectInitializer &ObjectInitializer )
        : Super( ObjectInitializer )
    {
        PlayerControllerClass = ARPGPlayerController::StaticClass();
        DefaultPawnClass = ARPGCharacter::StaticClass();
    }
    

    这里,我们包含RPGPlayerController.h和RPGCharacter.h这两个头文件,这样我们就可以引用这些类。然后,在构造函数中,我们将设置这些类作为默认的PlayerController和Pawn。
    现在,如果你编译此代码,你必须去设置你的工程的默认游戏模式为你的新建立的游戏模式。要做到这一点,请到编辑->项目设置,找到默认游戏模式选项框,展开默认游戏模式下拉菜单,并选择RPGGameMode。
    然而,我们不一定要直接使用此类。相反,如果我们使用蓝图,我们可以将游戏模式的属性公开的,就可以在蓝图中更新这些公开的属性。
    所以,让我们创造一个新的蓝图,命名为DefaultRPGGameMode,让它继承自RPGGameMode:

    GameMode

    如果你打开这个新的蓝图,导航到默认值选项卡,你可以修改默认的Pawn,HUD,PlayerController以及更多的设置:

    GameMode

    然而,在我们测试我们新的Pawn和PlayerController之前还有一个额外的步骤。如果你现在运行游戏,你会看不见Pawn。事实上,运行时就像什么都没有发生一样。因为我们需要给我们的Pawn设置一个模型和必须设置一个摄像机跟随我们的Pawn。


    添加模型

    现在,我们只需要导入第三人称示例中的蓝色角色原型。要做到这一点,请创建一个新的基于第三人称游戏的示例,并将以下内容迁移:

    • HeroTPP
    • HeroTPP_AnimBlueprint
    • HeroTPP_Skeleton
    • IdleRun_TPP

    通过以下步骤将这些项目迁移到RPG项目中:

    1. 选中这些资源
    2. 右键单击其中任何一个资源,并选择迁移
    3. 单击确定
    4. 在RPG项目中保存这些资源的文件夹
    5. 单击确定

    现在,使用你RPG项目中的HeroTPP模型,让我们为我们的Pawn创建一个新的蓝图。创建一个新的蓝图并选择RPGCharacter作为父类,取名为FieldPlayer。
    首先,展开Mesh选项并选择HeroTPP作为Pawn的骨骼。
    然后,展开Animation选项并选择HeroTPP_AnimBlueprint作为Pawn的动作。
    最后,打开你的游戏模式的蓝图,选择新的FieldPlayer作为你的默认Pawn。
    现在,你可以看见你的角色,并且在移动时可以播放一个跑动的动作。
    然而,摄像机不会跟随这个角色。我们会通过创建一个自定义的摄像机来解决这个问题。


    创建摄像机组件

    首先,创建一个新类,选择CameraComponent作为父类,命名为RPGCameraComponent。
    RPGCamera
    接着,带开RPGCameraComponent.h并在类定义中使用如下代码:

    • RPGCameraComponent.h
    
    UCLASS( meta = ( BlueprintSpawnableComponent ) )
    class RPG_API URPGCameraComponent : public UCameraComponent
    {
        GENERATED_BODY()
    
    public:
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CameraProperties)
        float CameraPitch;
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CameraProperties)
        float CameraDistance;
    
        virtual void GetCameraView( float DeltaTime, FMinimalViewInfo &DesiredView ) override;
    };
    

    让我们看看这一切意味着什么。
    首先,在UCLASS宏中,我们添加了这行:

    
    {
    UCLASS( meta = ( BlueprintSpawnableComponent ) )    
    }
    

    这使得我们可以再Pawn蓝图中添加我们的自定义组件。
    接着,我们定义了两个字段,CameraPitch和CameraDistance。

    • CameraPitch控制摄像机视角
    • CameraDistance控制摄像机距离

    并将这两个字段添加到CameraProperties这个类别下,这个字段的属性为EditAnywhere和BlueprintReadWrite
    最后,我们重写了负责计算摄像机位置,旋转和其他各种属性的GetCameraView函数。当具有此组件的pawn设置为当前视图目标时,虚幻会调用这个函数去定位游戏摄像机。
    接下来,打开RPGCameraComponent.cpp并添加以下代码:

    • RPGCameraComponent.cpp
    
    void URPGCameraComponent::GetCameraView( float DeltaTime, FMinimalViewInfo& DesiredView )
    {
        UCameraComponent::GetCameraView( DeltaTime, DesiredView );
        DesiredView.Rotation = FRotator( CameraPitch, 0.0f, 0.0f );
        if( APawn* OwningPawn = Cast<APawn>( GetOwner() ) )
        {
            FVector location = OwningPawn->GetActorLocation();
            location -= DesiredView.Rotation.Vector() * CameraDistance;
            DesiredView.Location = location;
        }
    }
    

    此函数将覆盖内置的GetCameraView函数。
    首先,它调用基类的GetCameraView函数来确保正确的设置了DesiredView。
    然后,它从CameraPitch创建了一个FRotator并将其分配给DesiredView的旋转。
    最后,它尝试将其所有者转换为APawn。如果OwningPawn不为空,则获取OwningPawn的位置,并减去摄像机的前向向量与CameraDistance的距离。然后将结果分配给DesiredView的位置。
    接着,我们需要给我们的Pawn添加摄像机组件,打开你前面章节创建的蓝图Pawn。在Components选项卡中,点击Add Component。当你搜索RPGCamera,你刚才创建的自定义组件会出现在列表中。
    在你添加了RPGCameraComponent组件后,滑动你的Details面板直到你看见了Camera Properties属性框。在这里,你可以输入任何你喜欢的值可以改变相机的俯仰程度和距离。但是开始你可以设置为50的俯仰值和600的距离值。
    现在,当你运行游戏,摄像机可以在俯视图中跟踪玩家。
    现在,我们有了一个可以探索游戏世界的角色,让我们来看看定义角色和伙伴成员。


    定义角色和敌人

    在上一章节中,我们介绍了如何使用数据表来导入自定义数据。在那之前,我们决定了数据如何在战斗中发挥。现在我们要结合那些来定义我们的游戏的角色,类别和遭遇敌人。


    类别

    回顾第一章的内容,在虚幻中设计一个RPG,我们设定了我们的角色有一下属性:

    • 生命值
    • 最大生命值
    • 魔法值
    • 最大魔法值
    • 攻击力
    • 防御
    • 幸运

    其中,我们可以先不定义生命值和魔法值,因为这两个值会在比赛期间变化。其他值都是角色预定义的基础值,这些就是我们将在数据表中定义的数据。如第一张”RPG入门”所述,我们也需要存储的值是50级(最高等级)。角色将有一些初始能力,还可以在升级时学习一些能力。
    我们将在角色类的电子表格中定义这些属性,以及类别的名称。所以我们的角色类表格结构看起来像下面这样:

    • 名称(字符串)
    • 初始最大生命值(整数)(1级时)
    • 最终最大生命值(整数)(50级时)
    • 初始最大魔法值(整数)(1级时)
    • 最终最大魔法值(整数)(50级时)
    • 初始攻击力(整数)(1级时)
    • 最终攻击力(整数)(50级时)
    • 初始防御力(整数)(1级时)
    • 最终防御力(整数)(50级时)
    • 初始幸运值(整数)(1级时)
    • 最终幸运值(整数)(50级时)
    • 初始能力列表(字符串数组)(1级时)
    • 学习能力列表(字符串数组)
    • 学习能力等级(整数数组)

    能力字符串数组将包含能力的ID(虚幻中是保留字段)。还有两列来存储学习能力信息——一列为所有可以学习的能力的ID数组,另一列为学习这些能力所需要的等级数组。

    在创造游戏的过程中,你应该考虑编写一个自定义工具来帮助管理数据,这样可以减少人为错误。但是,编写类似的工具不属于本书的范围。

    现在,我们不应该先为这些属性去创建电子表格,其实我们应该先在Unreal里创建类,再去创建数据表格。原因是,在填写数据时,没有好的文档记载怎么样在数据表的单元格中指定数组。然而在虚幻编辑器中我们可以编辑数组。所以我们简单的创建表格,然后使用虚幻的数组编辑器编辑。

    首先,像前面所做的一样,创建一个新类,它从哪里继承不是很重要。所以我们选择Actor类,命名为FCharacterClassInfo。

    打开FCharacterClassInfo.h,并使用以下代码替换类的定义:

    • FCharacterClassInfo.h
    
    USTRUCT( BlueprintType )
    struct FCharacterClassInfo : public FTableRowBase
    {
        GENERATED_USTRUCT_BODY()
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        FString Class_Name;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 StartMHP;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 StartMMP;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 StartATK;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 StartDEF;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 StartLuck;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 EndMHP;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 EndMMP;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 EndATK;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 EndDEF;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        int32 EndLuck;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        TArray<FString> StartingAbilities;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        TArray<FString> LearnedAbilities;
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "ClassInfo" )
        TArray<int32> LearnedAbilityLevels;
    };
    

    你应该很熟悉大部分的内容,但是最后三个字段你可能不认识。他们都是TArray类型,这是虚幻提供的动态数组类型。从根本上说,TArray和C++的数组不同,他可以动态的添加元素和移除元素

    再次编译代码后,你可以通过右键点击Content Browser,然后选择Create Advanced Asset->Miscellaneous->Data Table,来创建一个数据表格。然后,在下拉列表中选择Character Class Info,为你的数据表起个名字,然后双击打开它,你会看到下面的画面:

    DataTable

    如果Row Editor窗格为空的,你可能需要重启你的虚幻编辑器

    要添加新条目,请点击Add按钮。通过向Rename字段里输入文字,然后点击Enter键来给新条目命名。

    添加条目后,可以在Data Table窗格中选择该条目,然后在Row Editor穿个对它的属性进行编辑。

    我们在列表中添加一个Soldier类。我们将它命名为S1(我们将使用它来引用
    来自其他数据表的角色类),它具有以下属性:

    • Class name: Soldier
    • Start MHP: 100
    • Start MMP: 100
    • Start ATK: 5
    • Start DEF: 0
    • Start Luck: 0
    • End MHP: 800
    • End MMP: 500
    • End ATK: 20
    • End DEF: 10
    • End Luck: 10
    • Starting abilities: 现在为空
    • Learned abilities: 现在为空
    • Learned ability levels: 现在为空

    角色

    让我们来看看角色类的定义,大部分的战斗相关数据已经在character类别里定义了,角色本身会变的非常简单。事实上,现在我们的角色只需要定义两个事情:角色名称和角色引用的类别ID

    首先,让我们先来看看角色数据的头文件,FCharacterInfo.h:

    • FCharacterInfo.h
    
    USTRUCT(BlueprintType)
    struct FCharacterInfo : public FTableRowBase
    {
        GENERATED_USTRUCT_BODY()
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "CharacterInfo" )
        FString Character_Name;
        UPROPERTY( BlueprintReadOnly, EditAnywhere, Category = "CharacterInfo" )
        FString Class_ID;
    };
    

    和前面做的一样,我们只定义了两个字段(Character_Name和Class_ID)。

    在编译后,创建一个数据表格(Data Table)并选择CharacterInfo作为类别,添加一个新的条目取名Character_Name为S1,你也可以取一个你喜欢的名字,但是class
    ID必须填写S1(在之前我们定义了名称为S1的小兵类别)


    敌人

    至于敌人,我们不是单独定义个角色和单独的类别信息。我们会吧两个部分的信息简单的结合起来。作为一个敌人,通常不会处理获得经验和升级,所以我们可以省略这部分相关的数据。除此之外,敌人也不会像玩家一样消耗MP,我们也可以省略与此相关的数据。

    因为上面介绍的那些原因,我们的敌人数据会包含以下属性:

    • 名称(整数)
    • 最大生命值(整数)
    • 攻击力(整数)
    • 防御力(整数)
    • 幸运值(整数)

    现在,你应该了解如何构造这个类的数据。
    先让我们看看这个结构的头文件:

    • FEnemieInfo.h
    
    USTRUCT( BlueprintType )
    struct FEnemyInfo : public FTableRowBase
    {
        GENERATED_USTRUCT_BODY()
        UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "EnemyInfo" )
        FString EnemyName;
        UPROPERTY( BlueprintReadOnly, EditAnywhere, Category = "EnemyInfo" )
        int32 MHP;
        UPROPERTY( BlueprintReadOnly, EditAnywhere, Category = "EnemyInfo" )
        int32 ATK;
        UPROPERTY( BlueprintReadOnly, EditAnywhere, Category = "EnemyInfo" )
        int32 DEF;
        UPROPERTY( BlueprintReadOnly, EditAnywhere, Category = "EnemyInfo" )
        int32 Luck;
        UPROPERTY( BlueprintReadOnly, EditAnywhere, Category = "EnemyInfo" )
        TArray<FString> Abilities;
    };
    

    在编译之后,创建一个新的数据表格Data Table)并选择EnemyInfo作为类别,添加一个名叫S1的新条目,新条目具有以下属性:

    • Enemy name: Goblin
    • MHP: 100
    • ATK: 5
    • DEF: 0
    • Luck: 0
    • Abilities: 现在为空

    现在我们有了一个角色数据,一个角色类别和一个角色可以战斗的敌人。下一步,我们将会开始追踪那些角色是活动的和他们当前统计数据是什么。


    伙伴成员

    在我们可以追踪伙伴成员前,我们需要一种方法来追踪一个角色的状态,比如说角色还有多少HP,角色穿了什么装备。

    要做到这一点,我们需要新创建一个类命名为GameCharacter,像往常一样创建一个新类,但这次需要选择Object作为父类。

    此头文件的代码会和以下代码一样:

    • GameCharacter.h
    
    #include "Data/FCharacterInfo.h"
    #include "Data/FCharacterClassInfo.h"
    #include "GameCharacter.generated.h"
    
    UCLASS( BlueprintType )
    class RPG_API UGameCharacter : public UObject
    {
        GENERATED_BODY()
    public:
        FCharacterClassInfo* ClassInfo;
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CharacterInfo )
        FString CharacterName;
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CharacterInfo )
        int32 MHP;
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CharacterInfo )
        int32 MMP;
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CharacterInfo )
        int32 HP;
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CharacterInfo )
        int32 MP;
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CharacterInfo )
        int32 ATK;
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CharacterInfo )
        int32 DEF;
    
        UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = CharacterInfo )
        int32 LUCK;
    
    public:
    
        static UGameCharacter* CreateGameCharacter( FCharacterInfo* characterInfo, UObject* outer );
    
    public:
    
        void BeginDestroy() override;
    };
    

    现在我们角色的类信息、角色的名称、角色当前的统计数据。之后我们会用UCLASSUPROPERTY宏去暴露信息给蓝图。在这之后我们会添加一些在战斗系统中用到的信息。

    GameCharacter.cpp的代码会像这样:

    • GameCharacter.cpp
    
    UGameCharacter::UGameCharacter( const class FObjectInitializer& objectInitializer )
        :Super( objectInitializer )
    {
    }
    
    UGameCharacter* UGameCharacter::CreateGameCharacter( FCharacterInfo* characterInfo, UObject* outer )
    {
        UGameCharacter* character = NewObject<UGameCharacter>( outer );
    
        // locate character classes asset 
        UDataTable* characterClasses = Cast<UDataTable>( StaticLoadObject( UDataTable::StaticClass(), NULL, TEXT( "DataTable'/Game/Data/CharacterClasses.CharacterClasses'" ) ) );
    
        if( characterClasses == NULL )
        {
            UE_LOG( LogTemp, Error, TEXT( "Character classes datatable not found!") );
        }
        else
        {
            character->CharacterName = characterInfo->Character_Name;
            FCharacterClassInfo* row = characterClasses->FindRow<FCharacterClassInfo>( *( characterInfo->Class_ID ), TEXT( "LookupCharacterClass" ) );
            character->ClassInfo = row;
            character->MHP = character->ClassInfo->StartMHP;
            character->MMP = character->ClassInfo->StartMMP;
            character->HP = character->MHP;
            character->MP = character->MMP;
            character->ATK = character->ClassInfo->StartATK;
            character->DEF = character->ClassInfo->StartDEF;
            character->LUCK = character->ClassInfo->StartLuck;
        }
        return character;
    }
    
    void UGameCharacter::BeginDestroy()
    {
        Super::BeginDestroy();
    }
    

    UGameCharacter类的CreateGameCharacter方法接收一个从DataTable返回的指向FCharacterInfo的指针和产生这个Character的对象,用于传递给NewObject函数。然后尝试用一个路径找这个类的DataTable,接着如果结果不为空,则从DataTable中正确读取了一行数据,并被储存。接着用这些读取的数据来初始化Character的统计信息和CharacterName字段。在上面的代码,你可以看到角色DataTable的所在路径,这个路径你可以通过右键点击DataTable,然后选择Copy Reference选项,然后你就可以在你的代码中粘贴路径了。

    虽然现在的角色光秃秃的,但是他可以使用。接下来我们要存储这些角色到当前的伙伴列表。


    GameInstance类

    我们已经创建了一个GameMode(游戏模式)类,这个类看起来是我们追踪和存储伙伴成员的完美地方,是吧?

    然而,GameMode(游戏模式)在关卡不同关卡读取时不会保存数据,除非你把这些数据信息存储到了磁盘,每当你到了一个新的区域你将会丢失你的所有数据。

    下面我们来介绍一下为了解决这种问题的GameInstance类,font color=#191970 size=3>AGameInstance不像GameMode(游戏模式),不论关卡读取还是做些什么,他一直存在在整个游戏过程中。我们需要创建一个新的GameInstance类来持续追踪和存储伙伴成员的信息。

    创建一个新类,这一次我们选择GameInstance作为父类(你需要在查找功能中查找那个类),将它取名为RPGGameInstance。

    在这个头文件中,我们需要添加一个用来存储UGameCharacter指针的TArray,一个用来确定游戏已经被初始化的标志位和Init函数:

    • RPGGameInstance.h
    
    UCLASS()
    class RPG_API URPGGameInstance : public UGameInstance
    {
        GENERATED_BODY()
        URPGGameInstance( const class FObjectInitializer& ObjectInitializer );
    
    public:
    
        TArray<UGameCharacter*> PartyMembers;
    
    protected:
    
        bool isInitialized;
    
    public:
    
        void Init();
    
    };
    

    在游戏实例的Init函数中,我们会添加一个单个默认的伙伴成员并且设置isInitialized标志位为true

    • RPGGameInstance.cpp
    
    void URPGGameInstance::Init()
    {
        if( this->isInitialized ) return;
        this->isInitialized = true;
    
        // locate characters asset
        UDataTable* characters = Cast<UDataTable>( StaticLoadObject( UDataTable::StaticClass(), NULL, TEXT( "DataTable'/Game/Data/Characters.Characters'" ) ) );
    
        if( characters == NULL )
        {
            UE_LOG( LogTemp, Error, TEXT( "Characters data table not found!" ) );
            return;
        }
    
        // locate character
        FCharacterInfo* row = characters->FindRow<FCharacterInfo>( TEXT( "S1" ), TEXT( "LookupCharacterClass" ) );
    
        if( row == NULL )
        {
            UE_LOG( LogTemp, Error, TEXT( "Character ID 'S1' not found!" ) );
            return;
        }
    
        // add character to party
        this->PartyMembers.Add( UGameCharacter::CreateGameCharacter( row, this ) );
    }
    

    在虚幻中设置GameInstance,打开Edit->Project Settings跳转到Maps & Modes,向下滑动到Game Instance窗格,在下拉菜单中选择RPGGameInstance。最后,我们重写GameMode(游戏模式)的BeginPlay函数中调用这个Init方法:

    • RPGGameInstance.cpp
    
    // RPGGameMode.h
    virtual void BeginPlay() override;
    
    // RPGGameMode.cpp
    void ARPGGameMode::BeginPlay()
    {
        Cast<URPGGameInstance>( GetGameInstance() )->Init();
    }
    

    现在我们有了一个活动的伙伴成员列表,是时候去实现战斗引擎了。


    回合战斗

    正如我们第1章“虚幻引擎RPG设计入门”所讲的,我们的战斗是回合制战斗。所有的角色先要选择一个要执行的动作。然后所有角色按照顺序依次执行动作。

    战斗会分为两个阶段:

    • 决策,所有角色决定他们的行动方案。
    • 行动,所有角色按照他们的行动方案执行。

    我们需要创建一个为我们处理战斗的类,取名为CombatEngine

    • CombatEngine.h
    
    #include "RPG.h"
    #include "GameCharacter.h"
    
    enum class CombatPhase : uint8
    {
        CPHASE_Decision,
        CPHASE_Action,
        CPHASE_Victory,
        CPHASE_GameOver,
    };
    
    class RPG_API CombatEngine
    {
    
    public:
    
        TArray<UGameCharacter*> combatantOrder;
        TArray<UGameCharacter*> playerParty;
        TArray<UGameCharacter*> enemyParty;
        CombatPhase phase;
    
    protected:
    
        UGameCharacter* currentTickTarget;
        int tickTargetIndex;
    
    public:
    
        CombatEngine( TArray<UGameCharacter*> playerParty, TArray<UGameCharacter*> enemyParty );
        ~CombatEngine();
        bool Tick( float DeltaSeconds );
    
    protected:
    
        void SetPhase( CombatPhase phase );
        void SelectNextCharacter();
    };
    

    这个类很长,我要一一解释。

    我们的战斗引擎会在遭遇敌人时创建并且会在战斗结束时删除。
    一个战斗CombatEngine的实例保存着三个TArrays:一个是用来存储战斗顺序(所有战斗参与者的顺序列表,所有参与者会依次轮流行动),另一个是玩家列表,第三个是敌人列表。这个实例也持续追踪着CombatPhase,战斗有两个主要的阶段:DecisionAction,战斗中的每一轮都从Decision阶段开始。在这个阶段,所有的角色决定他们的行动方案。然后战斗转换为Action阶段。在这个阶段中,所有的角色按照顺序执行之前决定的行动方案。

    GameOverVictory会在所有敌人全部死亡或者玩家全部死亡时分别转换到这两个状态中。(这就是为什么我们要将敌人列表和玩家列表分成两个单独的列表)

    CombatEngine类定义了一个Tick方法,只要战斗没有结束,游戏模式类会每一帧都调用此方法。当战斗结束时,这个方法返回结果为ture(没有结束返回false),这个方法将上一帧的持续时间作为参数。

    还有currentTickTargettickTargetIndex,在DecisionAction阶段,我们会保存一个指向单个角色的指针。比如说,在Decision阶段,在开始时指针会指向战斗顺序列表中的第一个角色。在每一帧中,都会有一个函数让这个角色做出决定。如果返回ture表示角色已经完成了决定,如果返回false表示角色还没有决定。如果这个函数返回了true这个指针会指向列表中的下一个角色,然后这样一直持续到所有角色都昨晚了决定。之后战斗转到到Action阶段。

    这个CPP文件很大,我们拆分成小块来看。我们先来看看构造函数和析构函数。

    • CombatEngine.cpp
    
    CombatEngine::CombatEngine( TArray<UGameCharacter*> playerParty, TArray<UGameCharacter*> enemyParty )
    {
        this->playerParty = playerParty;
        this->enemyParty = enemyParty;
        // first add all players to combat order
        for( int i = 0; i < playerParty.Num(); i++ )
        {
            this->combatantOrder.Add( playerParty[i] );
        }
        // next add all enemies to combat order
        for( int i = 0; i < enemyParty.Num(); i++ )
        {
            this->combatantOrder.Add( enemyParty[i] );
        }
        this->tickTargetIndex = 0;
        this->SetPhase( CombatPhase::CPHASE_Decision );
    }
    
    CombatEngine::~CombatEngine()
    {
    
    }
    

    构造函数首先分配playerPartyenemyParty这两个字段,然后将所有玩家依次加入到战斗顺序列表中,再将敌人依次加入到战斗顺序列表中。最后,设置目标索引为0(即战斗顺序列表的第一个角色)和战斗阶段为Decision阶段

    我们紧接着看看Tick方法:

    • CombatEngine.cpp
    
    bool CombatEngine::Tick( float DeltaSeconds )
    {
        switch( phase )
        {
        case CombatPhase::CPHASE_Decision:
            // todo: ask current character to make decision
            // todo: if decision made
            SelectNextCharacter();
            // no next character, switch to action phase
            if( this->tickTargetIndex == -1 )
            {
                this->SetPhase( CombatPhase::CPHASE_Action );
            }
            break;
        case CombatPhase::CPHASE_Action:
            // todo: ask current character to execute decision
            // todo: when action executed
            SelectNextCharacter();
            // no next character, loop back to decision phase
            if( this->tickTargetIndex == -1 )
            {
                this->SetPhase( CombatPhase::CPHASE_Decision );
            }
            break;
        // in case of victory or combat, return true (combat is finished)
        case CombatPhase::CPHASE_GameOver:
        case CombatPhase::CPHASE_Victory:
            return true;
            break;
        }
    
        // check for game over
        int deadCount = 0;
        for( int i = 0; i < this->playerParty.Num(); i++ )
        {
            if( this->playerParty[ i ]->HP <= 0 ) deadCount++;
        }
    
        // all players have died, switch to game over phase
        if( deadCount == this->playerParty.Num() )
        {
            this->SetPhase( CombatPhase::CPHASE_GameOver );
            return false;
        }
    
        // check for victory
        deadCount = 0;
        for( int i = 0; i < this->enemyParty.Num(); i++ )
        {
            if( this->enemyParty[ i ]->HP <= 0 ) deadCount++;
        }
    
        // all enemies have died, switch to victory phase
        if( deadCount == this->enemyParty.Num() )
        {
            this->SetPhase( CombatPhase::CPHASE_Victory );
            return false;
        }
    
        // if execution reaches here, combat has not finished - return false
        return false;
    }
    

    我们先看当前值阶段是处于哪个战斗阶段,如果处于Decision阶段我们只做了选择下一个角色这件事,如果没有角色可以选择了,则切换到Action阶段。如果处于Action阶段也是同样的逻辑,如果没有角色可以选择了,则循环切换回Decision阶段

    之后会调用角色的方法使得他们按顺序做决定或者执行动作。(注意:选择一下一个角色这个函数只能在完成决定后或者执行动作后调用一次。)

    GameOverVictory阶段,Tick返回true意味着战斗结束了。
    在战斗没有结束时,函数先检查是不是所有玩家都死亡了(检查战斗是不是失败了),然后检查了所有敌人是不是死亡了(检查战斗是不是胜利了)。这个两个阶段都会返回true表示战斗结束了。

    在函数的最后返回了false来表示战斗还没有结束。

    接下来我们来看看SetPhase函数:

    • CombatEngine.cpp
    
    void CombatEngine::SetPhase( CombatPhase phase )
    {
        this->phase = phase;
        switch( phase )
        {
        case CombatPhase::CPHASE_Action:
        case CombatPhase::CPHASE_Decision:
            // set the active target to the first character in the combat order
            this->tickTargetIndex = 0;
            this->SelectNextCharacter();
            break;
        case CombatPhase::CPHASE_Victory:
            // todo: handle victory
            break;
        case CombatPhase::CPHASE_GameOver:
            // todo: handle game over
            break;
        }
    }
    

    这是个设置战斗阶段的函数,当战斗阶段为Action后者Decision时,这个函数会设置tickTargetIndex为战斗顺序列表中的第一个。VictoryGameOver预留着各自的状态处理。

    最后我们来看SelectNextCharacter

    • CombatEngine.cpp
    
    void CombatEngine::SelectNextCharacter()
    {
        for( int i = this->tickTargetIndex; i < this->combatantOrder.Num(); i++ )
        {
            GameCharacter* character = this->combatantOrder[ i ];
            if( character->HP > 0 )
            {
                this->tickTargetIndex = i + 1;
                this->currentTickTarget = character;
                return;
            }
        }
        this->tickTargetIndex = -1;
        this->currentTickTarget = nullptr;
    }
    

    这个函数从当前tickTargetIndex位置开始按顺序向后找到一个没有死亡的角色。如果找到一个,就将tickTargetIndexcurrentTickTarget都设置为这个角色。如果没有找到,就将tickTargetIndex设置为-1,currentTickTarget设置为空指针(这意味着作战顺序列表里面已经没有存活的角色了)。

    现在还遗漏了一件非常重要的事情:角色还不能作出或者执行决定。

    让我们将这两个方法加入到GameCharacter类中,只是作为预留的方法。

    首先我们添加testDelayTimer字段,这个字段只作为测试用途。

    • GameCharacter.h
    protected:
        float testDelayTimer;

    接下来我们往类中添加几个方法。

    • GameCharacter.h
    public:
        void BeginMakeDecision();
        bool MakeDecision( float DeltaSeconds );
    
        void BeginExecuteAction();
        bool ExecuteAction( float DeltaSeconds );

    我们以同样的方式分离了DecisionAction,让他们各自拥有两个函数。第一个函数是告诉角色开始做决定或者开始执行动作,第二个函数的本质上是一直查询角色是否已经完成决定或者完成执行动作。

    这两个方法我们会在以后实现,现在,我们只是延迟一秒输出日志:

    • GameCharacter.cpp
    void UGameCharacter::BeginMakeDecision()
    {
        UE_LOG( LogTemp, Log, TEXT( "Character %s making decision" ), *this->CharacterName );
        this->testDelayTimer = 1;
    }
    
    bool UGameCharacter::MakeDecision( float DeltaSeconds )
    {
        this->testDelayTimer -= DeltaSeconds;
        return this->testDelayTimer <= 0;
    }
    void UGameCharacter::BeginExecuteAction()
    {
        UE_LOG( LogTemp, Log, TEXT( "Character %s executing action" ), *this->CharacterName );
        this->testDelayTimer = 1;
    }
    
    bool UGameCharacter::ExecuteAction( float DeltaSeconds )
    {
        this->testDelayTimer -= DeltaSeconds;
        return this->testDelayTimer <= 0;
    }

    我们还要添加一个指向战斗实例的指针。因为战斗引擎已经引用了角色类,角色类在引用战斗引擎会产生循环依赖。为了避免这个问题,我们需要在GameCharacter.h中添加前置声明。

    • GameCharacter.h
    class CombatEngine;

    然后,战斗引擎的include语句应该放在
    GameCharacter.cpp中而不是在头文件中。

    接下来,我们要用战斗引擎来调用DecisionAction的方法,我们要先在CombatEngine类中添加一个标志位:

    • CombatEngine.h
    bool waitingForCharacter;

    这个标志位将用于切换。例如,在BeginMakeDecisionMakeDecision之前切换。

    接下来,我们要更新Tick方法中的DecisionAction阶段。我们先来更新一下前面的Decision部分。

    • CombatEngine.cpp
    {
        if( !this->waitingForCharacter )
        {
            this->currentTickTarget->BeginMakeDecision();
            this->waitingForCharacter = true;
        }
    
        bool decisionMade = this->currentTickTarget->MakeDecision( DeltaSeconds );
        if( decisionMade )
        {
            SelectNextCharacter();
            // no next character, switch to action phase
            if( this->tickTargetIndex == -1 )
            {
                this->SetPhase( CombatPhase::CPHASE_Action );
            }
        }
    } 
    break;

    如果waitingForCharacterfalse,它会调用BeginMakeDecision方法并且设置waitingForCharactertrue

    注意整个括号括起来的case语句,如果你不加这个括号,case语句会在编译时报decisionMade初始化被跳过的错误。

    接着调用了MakeDecision方法并传递了一帧的时间作为参数。如果这个方法返回true,将会选择下一个角色。返回false就切换到Action阶段。

    Action阶段和上面的代码几乎相同:

    • CombatEngine.cpp
    {
        if( !this->waitingForCharacter )
        {
            this->currentTickTarget->BeginExecuteAction();
            this->waitingForCharacter = true;
        }
        bool actionFinished = this->currentTickTarget->ExecuteAction( DeltaSeconds );
        if( actionFinished )
        {
            SelectNextCharacter();
            // no next character, switch to action phase
            if( this->tickTargetIndex == -1 )
            {
                this->SetPhase( CombatPhase::CPHASE_Decision );
            }
        }
    }
    break;

    接着我们要更新一下SelectNextCharacter方法,需要在这个方法中将waitingForCharacter设置回false

    • CombatEngine.cpp
    void CombatEngine::SelectNextCharacter()
    {
        this->waitingForCharacter = false;
        // ...(原先代码)
    }

    最后,我们还要完善一些细节:我们的战斗引擎需要设置所有的角色的CombatInstance的指针指向自己,我们需要在构造函数里做这些。然后我们还需要在析构函数中清空这些指针和敌人的指针:

    • CombatEngine.cpp
    CombatEngine::CombatEngine( TArray<UGameCharacter*> playerParty, TArray<UGameCharacter*> enemyParty )
    {
        // ...
        for( int i = 0; i < this->combatantOrder.Num(); i++ )
        {
            this->combatantOrder[i]->combatInstance = this;
        }
        this->tickTargetIndex = 0;
        this->SetPhase( CombatPhase::CPHASE_Decision );
    }
    
    CombatEngine::~CombatEngine()
    {
        // free enemies
        for( int i = 0; i < this->enemyParty.Num(); i++ )
        {
            this->enemyParty[i] = nullptr;
        }
    
        for( int i = 0; i < this->combatantOrder.Num(); i++ )
        {
            this->combatantOrder[i]->combatInstance = nullptr;
        }
    }

    现在战斗引擎到功能已经完整了,我们还需要把它挂钩到游戏中。我们要在游戏模式中去触发战斗和更新战斗。

    所以在我们的游戏模式类中,我们需要添加个指针去指向当前战斗。然后重写游戏模式类的Tick方法。此外还的保存一个追踪角色的列表(修饰符要用UPROPERTY,这样敌人就可以被垃圾回收了):

    • RPGGameMode.h
    UCLASS()
    class RPG_API ARPGGameMode : public AGameMode
    {
        GENERATED_BODY()
    
        ARPGGameMode( const class FObjectInitializer& ObjectInitializer );
        virtual void Tick( float DeltaTime ) override;
    
    public:
    
        CombatEngine* currentCombatInstance;
        TArray<UGameCharacter*> enemyParty;
    };

    接着在cpp文件中我们来实现它的Tick方法:

    • RPGGameMode.cpp
    void ARPGGameMode::Tick( float DeltaTime )
    {
        if( this->currentCombatInstance != nullptr )
        {
            bool combatOver = this->currentCombatInstance->Tick( DeltaTime );
            if( combatOver )
            {
                if( this->currentCombatInstance->phase == CombatPhase::CPHASE_GameOver )
                {
                    UE_LOG( LogTemp, Log, TEXT( "Player loses combat, game over" ) );
                }
                else if( this->currentCombatInstance->phase == CombatPhase::CPHASE_Victory )
                {
                    UE_LOG( LogTemp, Log, TEXT( "Player wins combat" ) );
                }
                // enable player actor
                UGameplayStatics::GetPlayerController( GetWorld(), 0 )->SetActorTickEnabled( true );
    
                delete( this->currentCombatInstance );
                this->currentCombatInstance = nullptr;
                this->enemyParty.Empty();
            }
        }
    }

    我们现在只检查是否有当前战斗实例。如果有,则调用战斗实例的Tick方法。如果返回true,则检查状态是胜利了还是失败了。(现在我们只是输出了日志在控制台)。然后,删除了了战斗实例,设置当前战斗实例为空,然后清空了敌方的角色列表(因为列表有UPROPERTY修饰符,会使列表内的敌人自动被垃圾回收),在这我们还启用了玩家的Tick方法。(我们会在战斗开始时禁用玩家的Tick方法,所以玩家会在战斗时冻结在原地)

    我们也已经准备好遭遇敌人了,但是现在没有敌人和我们战斗。

    我们已经定义了敌人信息的表,但是我们的GameCharacter类还不支持用EnemyInfo来初始化敌人(前面我们只实现了初始化玩家)。

    为了解决这个问题,我们需要在GameCharacter类中创建一个工厂方法(确定你也在头部添加了EnemyInfo类的include语句):

    • GameCharacter.h
    static UGameCharacter* CreateGameCharacter( FEnemyInfo* enemyInfo, UObject* outer );

    我们也得实现这个重载方法:

    • GameCharacter.cpp
    UGameCharacter* UGameCharacter::CreateGameCharacter( FEnemyInfo* enemyInfo, UObject* outer )
    {
        UGameCharacter* character = NewObject<UGameCharacter>( outer );
        character->CharacterName = enemyInfo->EnemyName;
        character->ClassInfo = nullptr;
        character->MHP = enemyInfo->MHP;
        character->MMP = 0;
        character->HP = enemyInfo->MHP;
        character->MP = 0;
        character->ATK = enemyInfo->ATK;
        character->DEF = enemyInfo->DEF;
        character->LUCK = enemyInfo->Luck;
        return character;
    }

    这是一个比较简单的实现,简单分配了名称,ClassInfo为空(因为敌人并没有与他们关联的类)和其他的统计数据(MMPMP都设置为0,因为敌人不用消耗MP)。

    为了测试我们的战斗系统,我们在RPGGameMode.h中创建了一个函数,这个函数可以在虚幻控制台调用。

    • RPGGameMode.h
    UFUNCTION(exec)
    void TestCombat();

    UFUNCTION(exec)宏可以让这个函数可以在虚幻控制台中使用命令调用。

    RPGGameMode.cpp中此方法的实现如下:

    • RPGGameMode.cpp
    void ARPGGameMode::TestCombat()
    {
        // locate enemies asset
        UDataTable* enemyTable = Cast<UDataTable>( StaticLoadObject
            ( UDataTable::StaticClass()
            , NULL
            , TEXT( "DataTable'/Game/Data/Enemies.Enemies'" ) 
            ) );
    
        if( enemyTable == NULL )
        {
            UE_LOG( LogTemp, Error, TEXT( "Enemies data table not found!" ) );
            return;
        }
    
        // locate enemy
        FEnemyInfo* row = enemyTable->FindRow<FEnemyInfo>( TEXT( "S1" ), TEXT( "LookupEnemyInfo" ) );
    
        if( row == NULL )
        {
            UE_LOG( LogTemp, Error, TEXT( "Enemy ID 'S1' not found!" ) );
            return;
        }
    
        // disable player actor
        UGameplayStatics::GetPlayerController( GetWorld(), 0 )->SetActorTickEnabled( false );
    
        // add character to enemy party
        UGameCharacter* enemy = UGameCharacter::CreateGameCharacter( row, this );
        this->enemyParty.Add( enemy );
    
        URPGGameInstance* gameInstance = Cast<URPGGameInstance>( GetGameInstance() );
    
        this->currentCombatInstance = new CombatEngine( gameInstance->PartyMembers, this->enemyParty );
    
        UE_LOG( LogTemp, Log, TEXT( "Combat started" ) );
    }

    在这我们创建了一个敌人的DataTable,并选择了ID为S1的敌人创造了一个GameCharacter,紧接着创造了一个敌人的列表来添加这些敌人。然后创建了一个CombatEngine的实例传递给了玩家方,敌人的列表传给了敌方。我们还必须在战斗开始的时候禁用Tick方法,来停止对玩家的更新。

    最后,我们必须测试一下战斗引擎,开始游戏后按键盘的(~)键来调出控制台命令行窗口,输入TestCombat然后按Enter键。

    在输出窗口,你可以看到一些和下面信息类似的信息:

    LogTemp: Combat started
    LogTemp: Character Kumo making decision
    LogTemp: Character Goblin making decision
    LogTemp: Character Kumo executing action
    LogTemp: Character Goblin executing action
    LogTemp: Character Kumo making decision
    LogTemp: Character Goblin making decision
    LogTemp: Character Kumo executing action
    LogTemp: Character Goblin executing action
    LogTemp: Character Kumo making decision
    LogTemp: Character Goblin making decision
    LogTemp: Character Kumo executing action
    LogTemp: Character Goblin executing action
    LogTemp: Character Kumo making decision

    首先这些信息表明战斗引擎正在像预期一样的运行。所有的角色都做出一个决定,然后去执行决定。接着他们又会做出决定,然后继续去执行决定,然后一直持续下去。因为没有人做任何事情(更不会造成任何伤害),所以战斗会一直持续下去。

    现在还有两个问题围绕着我们:第一,就是前面提到的问题,没有一个角色真正的做任何事情。此外,玩家角色需要一个与敌人不同的方式来作出决定(玩家角色需要一个UI去选择动作来作出决定,相反敌人角色需要自动的作出决定)

    我们会在解决决策问题之前先解决第一个问题。


    执行动作

    为了能让角色执行动作,我们要把所有的战斗动作归为一个通用的接口。我们现在已经有了映射这些接口的好地方。那就是角色的BeginExecuteActionExecuteAction这两个方法。

    让我们像下面一样创建一个新的接口ICombatAction

    • CombatAction.h
    #pragma once
    #include "GameCharacter.h"
    
    class UGameCharacter;
    
    class ICombatAction
    { 
    public:
    
        virtual void BeginExecuteAction( UGameCharacter* character ) = 0;
        virtual bool ExecuteAction( float DeltaSeconds ) = 0;
    };

    BeginExecuteAction接收一个指向正在执行动作的角色的指针
    ExecuteAction像之前一样,接收上一帧的时间作为参数

    接着我们创建一个新的动作类来实现这些接口。作为测试,我们在新类TestCombatAction中复制前面角色已经做的功能(也就是什么都没有,打印些日志):

    头文件的代码会是下面这样:

    • TestCombatAction.h
    #pragma once
    
    #include "ICombatAction.h"
    
    class TestCombatAction : public ICombatAction
    { 
    protected:
    
        float delayTimer;
    public:
    
        virtual void BeginExecuteAction( UGameCharacter* character ) override;
        virtual bool ExecuteAction( float DeltaSeconds ) override;
    };

    cpp代码会是下面这样:

    • TestCombatAction.cpp
    #include "RPG.h"
    #include "TestCombatAction.h"
    
    void TestCombatAction::BeginExecuteAction( UGameCharacter* character )
    {
        UE_LOG( LogTemp, Log, TEXT( "%s does nothing" ), *character->CharacterName );
        this->delayTimer = 1.0f;
    }
    
    bool TestCombatAction::ExecuteAction( float DeltaSeconds )
    {
        this->delayTimer -= DeltaSeconds;
        return this->delayTimer <= 0.0f;
    }

    接着,我们要修改角色类能够存储和执行这些动作。

    首先,将角色类中测试用的delayTimer字段替换成一个战斗动作的指针。然后在我们需要在创建决策系统时公开这个字段。

    • GameCharacter.h
    public:
        ICombatAction* combatAction;

    接着我们需要在决策函数中分配一个战斗动作,在执行函数中执行这个动作:

    • GameCharacter.cpp
    void UGameCharacter::BeginMakeDecision()
    {
        UE_LOG( LogTemp, Log, TEXT( "Character %s making decision" ), *( this->CharacterName ) );
        this->combatAction = new TestCombatAction();
    }
    
    bool UGameCharacter::MakeDecision( float DeltaSeconds )
    {
        return true;
    }
    
    void UGameCharacter::BeginExecuteAction()
    {
        this->combatAction->BeginExecuteAction( this );
    }
    
    bool UGameCharacter::ExecuteAction( float DeltaSeconds )
    {
        bool finishedAction = this->combatAction->ExecuteAction( DeltaSeconds );
        if( finishedAction )
        {
            delete( this->combatAction );
            return true;
        }
        return false;
    }

    BeginMakeDecision现在分配了一个TestCombatAction的实例,MakeDecision只是返回了trueBeginExecuteAction方法,用存储的动作调用了相同名称的方法,并且传递了这个角色的指针作为参数。最后,ExecuteAction函数,也使用存储的动作调用了同名的方法并且得到了个结果,如果结果是true则删除指针并且返回true,相反则返回false

    让我们再次测试一下新的代码,你会发现在输出窗口会输出同样的日志信息,但现在它的作用是说明做什么而不是怎么做。

    现在我们已经有个方法来存储和执行动作了,接着我们要来实现我们的角色决策系统了。


    决策

    我们会像之前做执行动作一样,为决策系统重新创建一个接口,类似于BeginMakeDecision/MakeDecision这样的模式。IDecisionMaker会像下面这样:

    • DecisionMaker.h
    #pragma once
    
    #include "GameCharacter.h"
    
    class UGameCharacter;
    
    class IDecisionMaker
    { 
    public:
    
        virtual void BeginMakeDecision( UGameCharacter* character ) = 0;
        virtual bool MakeDecision( float DeltaSeconds ) = 0;
    };

    然后,我们要创建TestDecisionMaker来实现接口:

    • TestDecisionMaker.h
    // TestDecisionMaker.h
    #pragma once
    
    #include "IDecisionMaker.h"
    
    class RPG_API TestDecisionMaker : public IDecisionMaker
    {
    public:
    
        virtual void BeginMakeDecision( UGameCharacter* character ) override;
        virtual bool MakeDecision( float DeltaSeconds ) override;
    };
    • TestDecisionMaker.cpp
    
    // TestDecisionMaker.CPP
    
    #include "RPG.h"
    #include "TestDecisionMaker.h"
    #include "../Actions/TestCombatAction.h"
    
    void TestDecisionMaker::BeginMakeDecision( UGameCharacter* character )
    {
        character->combatAction = new TestCombatAction();
    }
    
    bool TestDecisionMaker::MakeDecision( float DeltaSeconds )
    {
        return true;
    }

    接着我们要往角色类里面添加一个指向IDecisionMaker的指针,并且修改BeginMakeDecision/MakeDecision方法来使用决策类。

    • GameCharacter.h
    // GameCharacter.h
    public:
        IDecisionMaker* decisionMaker;
    • GameCharacter.cpp
    // GameCharacter.cpp
    void UGameCharacter::BeginDestroy()
    {
        Super::BeginDestroy();
        delete( this->decisionMaker );
    }
    
    void UGameCharacter::BeginMakeDecision()
    {
        this->decisionMaker->BeginMakeDecision( this );
    }
    
    bool UGameCharacter::MakeDecision( float DeltaSeconds )
    {
        return this->decisionMaker->MakeDecision( DeltaSeconds );
    }

    现在我们在BeginDestroy函数中删除决策类对象吗,决策类对象会在角色创建时分配,并且他们被摧毁前删除这个对象。

    最后一步当然是在构造函数中分配决策类对象,在所有角色类的构造函数中添加以下代码:

    • GameCharacter.cpp
    // GameCharacter.cpp
        this->decisionMaker = new TestDecisionMaker();

    重新运行游戏,再次测试战斗,你可以在输出窗口看到完全一样的输出。然而,有个很大的区别,现在可以实现不同的角色被分配不同的决策,并且选择决策可以方便的去分配战斗动作去执行。例如,现在我们很容易去测试一个对目标造成伤害的动作。但是在这之前,我们先对GameCharacter类做一些小小的改动。


    目标选择

    我们需要在GameCharacter类中添加个字段来标识这个是个角色、还是玩家、还是敌人。另外我们还要添加一个SelectTarget方法用来从当前战斗实例中的玩家列表或者敌人列表中,选择第一个存活的角色,怎么选择是取决去这个角色是玩家还是敌人。

    我们先在GameCharacter.h中添加一个isPlayer字段:

    • GameCharacter.h
        bool isPlayer;

    紧接着我们还要添加一个SelectTarget方法

    • GameCharacter.h
        UGameCharacter* SelectTarget();

    GameCharacter.cpp文件中我们需要在创建角色的函数中给这个字段赋值。(这是很简单的,因为我们的玩家和敌人拥有独立的创建函数)

    • GameCharacter.cpp
    UGameCharacter* CreateGameCharacter( FCharacterInfo* characterInfo, UObject* outer )
    {
        //...(原有代码)
        character->isPlayer = true;
        return character;
    }
    
    UGameCharacter* CreateGameCharacter( FEnemyInfo* enemyInfo, UObject* outer )
    {
        // ...(原有代码)
        character->isPlayer = false;
        return character;
    }

    接着我们需要定义SelectTarget方法:

    • GameCharacter.cpp
    UGameCharacter* UGameCharacter::SelectTarget()
    {
        UGameCharacter* target = nullptr;
    
        TArray<UGameCharacter*> targetList = this->combatInstance->enemyParty;
        if( !this->isPlayer )
        {
            targetList = this->combatInstance->playerParty;
        }
    
        for( int i = 0; i < targetList.Num(); i++ )
        {
            if( targetList[ i ]->HP > 0 )
            {
                target = targetList[i];
                break;
            }
        }
    
        if( target->HP <= 0 )
        {
            return nullptr;
        }
        return target;
    }

    首先计算出我们需要在哪个列表(玩家列表和敌人列表)中选择我们的目标,然后遍历列表去寻找一个没有死亡的目标。如果没有找到,这个函数返回一个空指针。


    造成伤害

    现在有了一个简单选择目标的方式,让我们修改TestCombatAction类,使这个类用这个简单的方式来选择目标,然后我们尝试对目标造成伤害。

    我们先添加两个字段来维护对角色和目标的引用并且让我们的构造函数接收一个GameCharacter目标作为参数:

    • TestCombatAction.h
    protected:
        UGameCharacter* character;
        UGameCharacter* target;
    public:
        TestCombatAction( UGameCharacter* target );

    下面是实现的代码:

    • TestCombatAction.cpp
    TestCombatAction::TestCombatAction( UGameCharacter* target )
    {
        this->target = target;
    }
    
    void TestCombatAction::BeginExecuteAction( UGameCharacter* character )
    {
        this->character = character;
    
        // target is dead, select another target
        if( this->target->HP <= 0 )
        {
            this->target = this->character->SelectTarget();
        }
    
        // no target, just return
        if( this->target == nullptr )
        {
            return;
        }
    
        UE_LOG( LogTemp, Log, TEXT( "%s attacks %s" ), *character->CharacterName, *target->CharacterName );
    
        target->HP -= 10;
        this->delayTimer = 1.0f;
    }

    首先在构造函数中对target进行赋值。然后在BeginExecuteAction方法中,先对character进行赋值,紧接着检查目标是否存活。如果目标已经死亡,就会调用我们刚刚创建的SelectTarget方法来获取一个新目标。如果获得的新目标也为空,这意味着函数返回为空,也就是说没有可用的目标了。相反如果找到了新目标,将会输出一条格式为[character] attacks [target]的日志,最后扣除目标一部分HP,然后设置delayTimer

    下一步就是修改我们的TestDecisionMaker去选择一个目标并且将这个目标传给TestCombatAction的构造函数,这是一个比较简单的修改:

    • TestDecisionMaker.cpp
    void TestDecisionMaker::BeginMakeDecision( UGameCharacter* character )
    {
        // pick a target
        UGameCharacter* target = character->SelectTarget();
        character->combatAction = new TestCombatAction( target );
    }

    现在你可以运行游戏,测试一次遭遇战斗,你会在输出窗口看到类似下面的信息:

    LogTemp: Combat started
    LogTemp: Kumo attacks Goblin
    LogTemp: Goblin attacks Kumo
    LogTemp: Kumo attacks Goblin
    LogTemp: Player wins combat

    现在,我们有了一个两方可以互相攻击,并且有一方会获胜的战斗系统

    下一步,我们要将这些与用户界面连接


    用UMG制作战斗UI

    首先,我们需要设置我们的工程以确保正确的引入了UMGSlate相关类。

    打开RPG.Build.cs(也就是[ProjectName].Build.cs)并且找到下面这行并修改成这样:

    • [ProjectName].Build.cs
    PublicDependencyModuleNames.AddRange( 
        new string[] { 
            "Core", 
            "CoreUObject",
            "Engine", 
            "InputCore", 
            "UMG", 
            "Slate", 
            "SlateCore" 
        } 
    );

    这句语句的意思是,将UMGSlateSlateCore添加到现有字符串数组。

    接着,打开RPG.h然后加入下面这几行代码:

    • RPG.h
    #include "Runtime/UMG/Public/UMG.h"
    #include "Runtime/UMG/Public/UMGStyle.h"
    #include "Runtime/UMG/Public/Slate/SObjectWidget.h"
    #include "Runtime/UMG/Public/IUMGModule.h"
    #include "Runtime/UMG/Public/Blueprint/UserWidget.h"

    现在编译这个工程,这会需要一点时间。

    接着,我们创建一个战斗UI的基类。基本上,我们使用这个基类通过定义Blueprint-implementable在函数头部来允许C++游戏代码与蓝图UMG代码通信,这个函数可以在蓝图里实现并用C++调用

    创建一个新类命名为CombatUIWidget并且选择UserWidget作为父类:

    • CombatUIWidget.h
    
    #include "GameCharacter.h"
    #include "Blueprint/UserWidget.h"
    #include "CombatUIWidget.generated.h"
    
    UCLASS()
    class RPG_API UCombatUIWidget : public UUserWidget
    {
        GENERATED_BODY()
    
    public:
    
        UFUNCTION( BlueprintImplementableEvent, Category = "Combat UI" )
        void AddPlayerCharacterPanel( UGameCharacter* target );
    
        UFUNCTION( BlueprintImplementableEvent, Category = "Combat UI" )
        void AddEnemyCharacterPanel( UGameCharacter* target );
    };

    大多数情况下,我们只会定义几个个函数。AddPlayerCharacterPanelAddEnemyCharacterPanel函数接收一个指向角色的指针和生成一个该角色的窗口控件。(用来显示当前角色的统计数据)。

    然后我们编译下代码,完成后返回编辑器,创建一个新的Widget Blueprint命名为CombatUI,创建完成后打开它。选择File->Reparent Blueprint并且选择CombatUIWidget作为父类。

    Designer界面中,创建两个Horizontal Box窗口控件并分别命名为enemyPartyStatusplayerPartyStatus,他们将会分别拥有很多玩家和敌人的子控件,去显示他们的角色统计数据。对于他们,一定要启用Is Variable选项框,他们就对蓝图来说是可用的变量,保存并编译蓝图。

    然后,我们要为玩家和敌人创建显示角色统计数据的控件,我们先要创建一个需要被其他空间继承的基础控件。

    创建一个新的Widget Blueprint命名为BaseCharacterCombatPanel,在这个蓝图中,添加一个新变量CharacterTarget并选择Game Character作为Object Reference类别。

    然后,我们要为玩家和敌人做各自的控件。

    创建一个新的Widget Blueprint命名为PlayerCharacterCombatPanel,设置新蓝图的父类为BaseCharacterCombatPanel

    Designer界面中,添加三个Text Block控件,一个为角色名称,另一个为角色HP,第三个为角色MP。我们通过在Details面板选择控件并且点击Bind,弹出的旁边的文本,来创建一个绑定:

    Create Binding

    这个操作将创建一个新的蓝图函数来负责生成文本。

    例如,想要绑定HP文本,你需要下列步骤:

    1. 拖拽Character Target变量到视图中,并且选择Get
    2. 拖拽这个节点的引脚并且在Variables->Character Info下选择Get HP
    3. 创建一个新的Format Text节点,设置Format字段为HP: {HP},然后连接Get HP的输出到Format Text节点的HP字段的输入。
    4. 连接Format Text节点的输出到Return节点的Return Value

    你可以重复以上步骤来创建角色名称和MP的文本。

    在你完成了PlayerCharacterCombatPanel之后,你可以用同样的步骤来创建EnemyCharacterCombatPanel,除了不要创建MP的文本块(就像前面讲的,敌人并不用消耗MP)。

    最终MP的展现视图的画面会像下面这样:

    MP文本快

    现在我们有了玩家和敌人的控件,让我们在CombatUI蓝图中实现AddPlayerCharacterPanelAddEnemyCharacterPanel函数。

    我们要先创建一个帮助函数来创建角色统计数据控件,函数命名为SpawnCharacterWidget并且加入下列输入参数:

    • Target Character(游戏角色引用类型)
    • Target Panel(面板控件引用类型)
    • Class(基础战斗角色面板类)

    这个函数需要执行下列步骤:

    1. 为传入的Class创建一个新控件
    2. 转换这个新控件为BaseCharacterCombatPanel类型
    3. 设置Character Target为输入的TargetCharacter
    4. 把这个新控件作为TargetPanel的子控件。

    蓝图最终会像下面这样:

    SpawnCharacterWidget

    然后,在CombatUI蓝图的事件视图中,右键点击添加EventAddPlayerCharacterPanelEventAddEnemyCharacterPanel事件,将他们各自挂钩一个SpawnCharacterWidget节点,将Target输出连接到Target
    Character
    输入并且将合适的变量连接到Target Panel的输入,如下:

    CombatUI Events

    最后在我们的游戏模式中的战斗开始的地方生成这个UI,并且在战斗结束的时候摧毁这个UI,在RPGGameMode的头文件中,添加一个UCombatUIWidget指针和一个创建这个战斗UI的类(我们可以选择一个蓝图控件来继承我们的CombatUIWidget类):

    • RPGGameMode.h
    UPROPERTY()
    UCombatUIWidget* CombatUIInstance;
    
    UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category = "UI" )
    TSubclassOf<class UCombatUIWidget> CombatUIClass;

    在我们的TestCombat函数中,我们如下创建我们的空间实例:

    • RPGGameMode.cpp
    
    this->CombatUIInstance = CreateWidget<UCombatUIWidget>( GetGameInstance(),
    this->CombatUIClass );
    this->CombatUIInstance->AddToViewport();
    
    for( int i = 0; i < gameInstance->PartyMembers.Num(); i++ )
    {
        this->CombatUIInstance->AddPlayerCharacterPanel( gameInstance->PartyMembers[i] );
    }
    
    for( int i = 0; i < this->enemyParty.Num(); i++ )
    {
        this->CombatUIInstance->AddEnemyCharacterPanel( this->enemyParty[i] );
    }
    

    上面的代码创建了窗口,然后添加到视图,接着分别为玩家和敌人调用他们的AddPlayerCharacterPanelAddEnemyCharacterPanel函数。

    在战斗结束时,我们需要从视图中移除窗口,并且设置引用为空,之后他们会被垃圾回收:

    • RPGGameMode.cpp
    this->CombatUIInstance->RemoveFromViewport();
    this->CombatUIInstance = nullptr;

    现在,如果你运行游戏,你可以看见哥布林和玩家的统计数据,他们的HP都会持续的减少直到哥布林的血量为0。然后界面消失了(因为战斗结束了)。

    下一步,我们要用玩家在UI上选择动作来代替自动决策。

    (未完待更新)

    展开全文
  • Unreal Eegine 4 C Slate 介绍用C和Slate创建菜单一 Slate的准备工作 使用AHUD 创建主菜单窗口 重新修改HUD 设置游戏模式 总结 Unreal Eegine 4 C++ Slate 介绍——用C++和Slate创建菜单(一)好记性不如烂笔头啊...

    Unreal Engine 4 C++ Slate 介绍——用C++和Slate创建菜单(一)

    好记性不如烂笔头啊,还是记录一下!


    这是教程的第一部分,会学习一些基础的东西。我们将创建一个非常基本的、没有任何样式的游戏菜单,只会简单的显示一个游戏标题,并提供两个按钮:Play GameQuit。在下一个教程中,我将开始实现可以在虚幻编辑器中编辑的Slate UI Styles,允许您在编辑器本身内调整菜单的外观和感觉。接下来,我们将开始进入数据绑定,这对于更新UI的数据非常有用 - 例如,如果您要在商店界面中设置页面,则可能需要一个文本块显示玩家目前在她的购物车有多少项物品。最后,我将以可用于创建更多动态,交互式菜单的方法结束,例如您在StrategyGame示例中看到的。

    Slate的准备工作

    这个项目中的示例中,我将假设您已经创建一个空白项目。有没有初学者内容不重要,我们并不会在本教程中使用它。如果你创建了你的项目,先为项目生成代码和Visual Studio或Xcode项目。本教程系列的剩余部分,我假定您的项目名称为SlateTutorials的情况下运行 - 请记住在适当的情况下将其替换为您自己的项目名称。在项目的源文件夹中,打开SlateTutorials.Build.cs文件,取消注释或添加以下行:

    PrivateDependencyModuleNames.AddRange(
        new string[] {
            "Slate",
            "SlateCore"
        }
    );

    当您尝试构建项目时,将添加必要的Slate库和头文件到您的项目路径。您现在可以编写您的第一个Slate UI!


    使用AHUD

    我们要做的第一件事情是向项目中添加一个新的HUD类,我们取名为MainMenuHUD:

    • MainMenuHUD.h
    
    #include "GameFramework/HUD.h"
    #include "MainMenuHUD.generated.h"
    
    /**
      * Provides an implementation of the game’s Main Menu HUD, which will embed and respond to events triggered
      * within SMainMenuUI.
      */
    UCLASS()
    class SLATETUTORIALS_API AMainMenuHUD : public AHUD
    {
        GENERATED_BODY()
        // Initializes the Slate UI and adds it as widget content to the game viewport.
        virtual void PostInitializeComponents() override;
    
        // Reference to the Main Menu Slate UI.
        TSharedPtr<class SMainMenuUI> MainMenuUI;
    
    public:
        // Called by SMainMenu whenever the Play Game! button has been clicked.
        UFUNCTION(BlueprintImplementableEvent, Category = "Menus|Main Menu")
        void PlayGameClicked();
    
        // Called by SMainMenu whenever the Quit Game button has been clicked.
        UFUNCTION(BlueprintImplementableEvent, Category = "Menus|Main Menu")
        void QuitGameClicked();
    };
    • MainMenuHUD.cpp
    // Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
    
    #include "SlateTutorials.h"
    #include "MainMenuHUD.h"
    
    void AMainMenuHUD::PostInitializeComponents()
    {
        Super::PostInitializeComponents();
    }

    我觉得这里的注释对一切做了很好的解释,并且如果你经常使用C++和Slate,对这部分应该已经非常熟悉了。如果你还没想好PlayGameClicked()QuitGameClicked()要做什么,你需要给他们加上UFUNCTION宏,这两个函数将是任意蓝图继承MainMenuHUD类就会触发的事件,我们可以用于做任何事情。比如说,我可以在使用SMainMenuUI时调用这两个函数。我看了很多教程(包括UE4 维基上的Slate, Hello!教程,这个教程对我帮助很多)都是在BeginPlay()函数来初始化UI。但我发现,在StrategyGame示例中是在PostInitializeComponents()函数中进行的初始化,我选择使用后者的方法来实现。


    创建主菜单窗口

    现在,我们需要往工程中添加另一个类:SMainMenuUI类,继承与SCompoundWidget类,这个类要比HUD类复杂,所以我先展示代码,后面再详细解释每个部分。

    • MainMenuUI.h
    // Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
    
    /**
      * MainMenuUI.h – Provides an implementation of the Slate UI representing the main menu.
      */
    
    #pragma once
    
    #include "SlateBasics.h"
    #include "MainMenuHUD.h"
    
    // Lays out and controls the Main Menu UI for our tutorial.
    class SLATETUTORIALS_API SMainMenuUI : public SCompoundWidget
    {
    
    public:
        SLATE_BEGIN_ARGS(SMainMenuUI)
        {}
        SLATE_ARGUMENT(TWeakObjectPtr<class AMainMenuHUD>, MainMenuHUD)
        SLATE_END_ARGS()
    
        // Constructs and lays out the Main Menu UI Widget.
        // args Arguments structure that contains widget-specific setup information.
        void Construct(const FArguments& args);
    
        // Click handler for the Play Game! button – Calls MenuHUD’s PlayGameClicked() event.
        FReply PlayGameClicked();
    
        // Click handler for the Quit Game button – Calls MenuHUD’s QuitGameClicked() event.
        FReply QuitGameClicked();
    
        // Stores a weak reference to the HUD controlling this class.
        TWeakObjectPtr<class AMainMenuHUD> MainMenuHUD;
    };

    大部分代码是很直接的。注意:并没有将类指定为UCLASS(),实际上这个类并不需要到处给蓝图。首先这个类继承自SCompoundWidgetSCompoundWidget是一个Slate控件,可以由其他控件组成——在Slate的API中有很多示例:SVerticalBox,SHorizontalBox,SOverlay等等。相反的控件是SLeafWidget,它不包含任何控件。

    
        SLATE_BEGIN_ARGS(SMainMenuUI)
        {}
    
        SLATE_ARGUMENT(TWeakObjectPtr<class AMainMenuHUD>, MainMenuHUD)
    
        SLATE_END_ARGS()
    
        // Constructs and lays out the Main Menu UI Widget.
        // args Arguments structure that contains widget-specific setup information.
        void Construct(const FArguments& args);
    

    如果你没有习惯运用虚幻的宏,这部分可能看起来很奇怪。这三个宏的作用是用来生成一个结构,其中包含在构建过程中的参数列表。您可能注意到了Construct函数的参数FArguments——这些宏就是在定义这个结构。在我们的示例中,我们只有一个参数,一个指向一个拥有这个类的AMainMenuHUD弱指针。如果你还不熟悉智能指针,这简单的意味着这个指针可以引用父类HUD,但是避免了一个强引用的循环(记住,我们的HUD类也引用了这个对象),强引用循环会导致即使这些引用已经不在使用的时候对象依然存在在内存中无法释放。

    void Construct(const FArguments& args);

    Construct()方法接收一个参数,我们使用SLATE_*宏对FArguments结构化,结构中包含了控件的所有参数。当我们创建控件时,将调用这个方法,您很快就可以看到如何布置窗口控件。

    // Click handler for the Play Game! button – Calls MenuHUD’s PlayGameClicked() event.
    FReply PlayGameClicked();
    
    // Click handler for the Quit Game button – Calls MenuHUD’s QuitGameClicked() event.
    FReply QuitGameClicked();

    这个两个方法是用来处理我们将要添加的按钮Play GameQuit Game,这两个函数整合FOnClicked事件,它只返回一个FReply(告诉引擎是否处理事件)不接收任何参数。如果我们想要的话,我们可以为这些方法指定一个参数。在后面的教程,我会教你如何绑定数据并且使用他们。

    继续我们的主菜单

    现在,我们需要去实现我们的主菜单控件。这将是一个相当容易的任务,因为我们只需要实现三个方法——但他们在刚开始来看是非常的奇怪(当用Slate进行工作时,我推荐你用良好的习惯保持缩进,缩进对于理解布局是非常有必要的)。

    • MainMenuUI.cpp
    // Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
    
    #include "SlateTutorials.h"
    #include "MainMenuUI.h"
    #include "Engine.h"
    
    BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
    void SMainMenuUI::Construct(const FArguments& args)
    {
        MainMenuHUD = args._MainMenuHUD;
    
        ChildSlot
        [
            SNew(SOverlay)
            + SOverlay::Slot()
            .HAlign(HAlign_Center)
            .VAlign(VAlign_Top)
            [
                SNew(STextBlock)
                .ColorAndOpacity(FLinearColor::White)
                .ShadowColorAndOpacity(FLinearColor::Black)
                .ShadowOffset(FIntPoint(-1, 1))
                .Font(FSlateFontInfo("Arial", 26))
                .Text(FText::FromString("Main Menu"))
            ]
            + SOverlay::Slot()
            .HAlign(HAlign_Right)
            .VAlign(VAlign_Bottom)
            [
                SNew(SVerticalBox)
                + SVerticalBox::Slot()
                [
                    SNew(SButton)
                    .Text(FText::FromString("Play Game!"))
                    .OnClicked(this, &SMainMenuUI::PlayGameClicked)
                ]
                + SVerticalBox::Slot()
                [
                    SNew(SButton)
                    .Text(FText::FromString("Quit Game"))
                    .OnClicked(this, &SMainMenuUI::QuitGameClicked)
                ]
            ]
        ];
    
    }
    END_SLATE_FUNCTION_BUILD_OPTIMIZATION
    
    FReply SMainMenuUI::PlayGameClicked()
    {
        if (GEngine)
        {
            GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Yellow, TEXT("PlayGameClicked"));
        }
    
        // actually the BlueprintImplementable function of the HUD is not called; uncomment if you want to handle the OnClick via Blueprint
        //MainMenuHUD->PlayGameClicked();
        return FReply::Handled();
    }
    
    FReply SMainMenuUI::QuitGameClicked()
    {
        if (GEngine)
        {
            GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Yellow, TEXT("QuitGameClicked"));
        }
    
        // actually the BlueprintImplementable function of the HUD is not called; uncomment if you want to handle the OnClick via Blueprint
        //MainMenuHUD->QuitGameClicked();
        return FReply::Handled();
    }

    你可以看到我所说的定义Slate布局,关于缩进对于尴尬的布局的作用。我们从两个事件处理函数看起。如前面所讲,他们首先会调用HUD上的蓝图事件,这样便可以使我们处理蓝图对这些按钮的点击的响应,然后它们返回了FReply::Handled()——这是让引擎知道我们已经处理了点击事件,所以它不需要对玩家的鼠标输入做任何其他处理。

    我们再来看看Construct()方法,如前面所讲,FArguments是用SLATE*宏定义的。在我们的示例中,它添加了一个方法,我们稍后将使用它来指定拥有此控件的HUD。我们首先要做的事情是捕获这个值,我们只需要将本地值设置为args包含的值就可以了。注意:args里面的实际名称是_MainMenuHUD,而不是MainMenuHUD_MainMenuHUD是实际设置的变量,所有通过SLATE_ARGUMENT宏传递的参数都遵循此规定。

    整个布局定义是由Epic写的一个非常漂亮的地方,就是利用运算符重载和其他酷炫功能来定义我们的UI布局。简单的说,我们使用[]运算符来定义作为我们定制(组合)控件的子节点控件。对于组合控件,我需要先调用SNew(WidgetClass)方法,然后使用+ WidgetClass::Slot()来增加控件。然后我们可以在该slot中使用[]为该插槽指定子项。

    SOverlay的第一个子控件是STextBlock,他与自他GUI API中的TextBlock或者Label非常相似。为此,我们指定了颜色、阴影颜色、阴影偏移量、字体和一些文本。所有这些都是相当显而易见的,但是请注意一下,我们在SNew后没有任何::Slot()调用,因为TextBlock是一个SLeafWidget,它不能包含子控件。所以我们没有任何插槽可以使用,因此我们只能指定自身属性。

    我们的第二个子控件是一个组合控件——SVerticalBox。垂直框(与它相对应的是水平框),将所有元素由上到下依次排列(对于水平框是从左到右),占用相同的控件。在SVerticalBox的插槽内(记住,SVerticalBox是个组合控件),我们指定两个参数的SButton实例。第一个参数是显示在按钮上的文本,第二个是事件处理的绑定函数(PlayGameClicked / QuitGameClicked),每当点击时会调用该函数。然后我们完成了布局的编码,记得用;号结尾


    重新修改HUD

    我们已经完成了主菜单的布局设置。是时候就把它绑定到我们的HUD上!返回调整PostInitializeComponents方法:

    • MainMenuHUD.cpp
    // Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
    
    #include "SlateTutorials.h"
    #include "MainMenuHUD.h"
    #include "MainMenuUI.h"
    #include "Engine.h"
    
    void AMainMenuHUD::PostInitializeComponents()
    {
        Super::PostInitializeComponents();
    
        SAssignNew(MainMenuUI, SMainMenuUI).MainMenuHUD(this);
    
        if (GEngine->IsValidLowLevel())
        {
            GEngine->GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(MainMenuUI.ToSharedRef()));
        }
    }

    这里的设置就比较简单了——在确保EngineViewport有效后,我们创建一个MainMenuUI的实例,然后将控件作为内容添加到游戏视图上!注意:SAssignNew添加MenuHUD——这又是我们SLATE_宏的结果,记得前面我们提到的SLATE_ARGUMENT宏的MenuHUD部分?它不仅设置了我们的变量(_MainMenuHUD),也同时生成了我们这里用到的MainMenuHUD(this)这个设置方法。


    设置游戏模式

    使用虚幻创建一个游戏模式继承自GameMode取名为WidgetGameMode,并修改成一下样子:

    • WidgetGameMode.h
    // Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
    
    #pragma once
    
    #include "GameFramework/GameMode.h"
    #include "WidgetGameMode.generated.h"
    
    UCLASS()
    class SLATETUTORIALS_API AWidgetGameMode : public AGameMode
    {
        GENERATED_UCLASS_BODY()
    
    public:
        AWidgetGameMode();
    
    };
    • WidgetGameMode.cpp
    // Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
    
    #include "SlateTutorials.h"
    #include "WidgetGameMode.h"
    #include "MainMenuHUD.h"
    
    AWidgetGameMode::AWidgetGameMode()
    {
        HUDClass = AMainMenuHUD::StaticClass();
    }

    然后在项目设置里选择Maps & Modes

    GameMode Seting


    总结

    在你的游戏模式上设置适当的游戏模式类,设置你的关卡使用你的新游戏模式,并运行! 如果一切顺利,你应该会显示如下图,运行正常(但有点丑陋)的游戏菜单! 不要担心,在下一个教程中,我们将开始设置样式,这将允许我们大幅改善菜单项的外观!

    运行示例

    展开全文
  • 本次学习内容为同一按键控制多张图片的切换 1.My Character–Event Graph 添加Event Dispatchers,指定按键(此处以键盘方向键作参考) 2.新建的Blueprint Widget-Graph-Event Graph 新建布尔变量Isflag判断按键...

    本次学习内容为同一按键控制多张图片的切换
    1.My Character–Event Graph 添加Event Dispatchers,指定按键(此处以键盘方向键作参考)
    在这里插入图片描述
    2.新建的Blueprint Widget-Graph-Event Graph 新建布尔变量Isflag判断按键动作并将其扩展为数组
    在这里插入图片描述
    3.Blueprint Widget-Graph-Event Graph 添加CustomEvent执行步骤2数组变量的递增
    在这里插入图片描述
    4.Blueprint Widget-Graph-Event Graph构建委托循环事件
    在这里插入图片描述
    5.Blueprint Widget-Designer点击图片绑定函数
    在这里插入图片描述
    6.上一步绑定的函数编辑,其他图片同理
    在这里插入图片描述

    展开全文
  • 学习之后,把自己所学记录一下,初学者可以参考一下。可能有些术语不太准确。。。 我尽量让图片显示清晰,所截的图片很多都是一部分,然后进行拼凑  导入模型,游戏设置 ,需要提前做好。...下面是直接播放动画,...
  • 原文链接 最近用Ogre结合Qt时发现了一个问题,就是Qt的按键消息响应。具体情况请看下面的转载内容: Qt的消息响应可重载Widget中的keyPressEvent、keyReleaseEvent、mousePressEvent、mouseReleaseEvent、...
  • Unreal网络架构

    2017-03-11 16:44:19
    最初的多玩家游戏是双玩家的调制解调器游戏,以DOOM为代表,而现在多玩家游戏已经进化成为大型的、持久的、交互形式更加自由的游戏,如Quake2,Unreal和Ultima Online,共享现实背后的技术已经有了巨大的进步。...
  • 本站文章均为 李华明Himi 原创,转载务必在明显处注明: 转载自【黑米GameDev街区】 原文链接: http://www.himigame.com/unreal-engine-game/2164.html首先Himi在这里解释下,为什么还是开篇… 原因主要有两点:...
  • 笔者进行Unreal开发已经半年多了,使用过其自带的UMG,但初认为这个东西灵活性有限,很难做出非常炫丽的效果。这里介绍一下我之前做的能够平滑移动的分类式菜单,算上是较深入地挖掘了UMG的功能。
  • UE4 蓝图快捷键

    2019-03-25 14:45:44
    原文连接:http://api.unrealengine.com/CHN/Engine/Blueprints/UserGuide/CheatSheet/index.html 蓝图内置了很多提高效率的快捷方式,随着您应用编辑器,很多快捷方式自然而然地就会用到,但是还有一些快捷方式则...
  • 这次的版本带来了数百个虚幻引擎 4 的更新,包括来自 GitHub 的社区成员们提交的 145 个改进!感谢所有为虚幻引擎 4 添砖加瓦贡献的人们: alk3ovation, Allegorithmic (Allegorithmic), Alwin Tom (alwintom), ...
  • 更多相关内容参考 UE4移动组件详解(一)——移动框架与实现原理 UE4移动组件详解(二)——移动同步机制 五.特殊移动模式的实现思路 这一章节不是详细的实现教程,只是给大家提供常见游戏玩法的一些设计思路...
  • Unreal角色技术指南

    2015-08-06 21:07:30
    注: 转自UN官方网站,买书、下视频,到头来发现还是官方的免费Tutorial写得最好, 本文适用于熟悉UDK操作,想继续深入本质原理的读者~ 很好的说明了Pawn,Controller的关系,许多问题感觉茅塞顿开,故转之~~ ...
  • 使火炬灯光闪烁 在内容浏览器中找到已经做好的Actor——火炬,右键→编辑或者双击,在资源编辑器中查看Actor火炬。再点击时间图表 滚轮缩放,右键拖动背景移动,左键拖动单个事件移动。 将事件三角形拖出 搜索术语...
  • 一、增加模型 1.如果没有门的模型,在世界大纲界面,资源浏览面板,选择新增,选择添加功能或内容包,选择内容包中的StarterContent。... 2.搜索 Door 选择门的模型双击打开进入模型编辑器,给门设置碰撞盒,为了有...
  • 注: 转自UN官方网站,买书、下视频,到头来发现还是官方的免费Tutorial写得最好, 本文适用于熟悉UDK操作,想继续深入本质原理的读者~ 很好的说明了Pawn,Controller的关系,许多问题感觉茅塞顿开,故转之~~ ...
1 2 3 4 5
收藏数 98
精华内容 39