第一,这篇是我翻译的虚幻4官网的新手编程教程,原文传送门,有的翻译不太好,但大体意思差不多,请支持我O(∩_∩)O谢谢。
第二,某些细节操作,这篇文章省略了,如果有不懂的,去看其他教程。
第三,没有C++编程经验,但有其他OOP语言经验的朋友。要先了解C++的基础。
这个向导是帮您了解怎么去写C++代码。别担心,虚幻4编程很有趣,不难开始。我们把虚幻C++理解为“辅助C++”,因为我们有很多特性去帮助每一个人更好的去使用C++。
在我们开始之前,你已经熟悉了C++编程语言或者其他编程语言这很重要。这篇文章假设你已经有了C++的编程经验,或者你已经有了C#,JAVA,JavaScript,你也可以发现很多相似之处。
如果你完全没有编程经验,我们也考虑到了!你可以使用蓝图可视化脚本向导走你自己的路(意思就是不用敲代码啦),你可以用蓝图来创建自己的游戏。
在虚幻4引擎中会写一些“朴实老旧的C++代码”,但是通过向导学习以及学习虚幻4引擎模型你会写的很好,balabala,加油。
虚幻引擎提供2种方法,C++代码和蓝图可视化脚本(后简称:蓝图),来新增游戏元素。使用C++,程序员添加基础游戏系统,让美术能够在其之上建立游戏元素。在这些情况下,C++程序员在工作中使用他们最爱的IDE(VS2013,VS2015,Xcode),美术在工作中使用虚幻蓝图编辑器。
游戏API和工作框架在这2种方法中,都是可用的。可以单独使用,但是要真正的发挥他们各自的力量,需要完美结合。我想说的是,最好的虚幻项目应该,程序员使用C++建立游戏空物体,美术使用空物体添加有趣的东西。
如我们所说,我们将要演示一下典型的工作流,程序员为美术提供空物体,在这种情况下,我们将用蓝图类扩展这个空物体。在这个C++类中,我们将添加几个属性,并且派生几个属性到虚幻引擎中,我们使用引擎提供的工具和C++宏命令,这些过程非常简单。
第一件事,我们将用虚幻引擎中的类向导创建基础C++类,这个类将在之后用蓝图扩展,下图显示向导第一步,我们新建一个Actor
第二步,修改类的名称,在这里我们用默认的类名称。
当你第一次新建类的时候,向导将生成新的文件夹并且打开你的IDE,为了是你能够编辑。下面是类向导自动生成的类代码,更多关于类向导的信息,去官网看去。
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMyActor();
// Called when the game starts or when spawned
virtual void BeginPlay() override;
// Called every frame
virtual void Tick( float DeltaSeconds ) override;
};
这个类向导生成的类中有BeginPlay() 和Tick()的特定重载,BeginPlay()是一个事件,当Actor进入游戏时触发,这个方法中可以初始化一些游戏逻辑或类属性。Tick()是每一帧都会调用,这里你可以加经常性的逻辑。然而如果你确定你不需要某些函数功能,最好在代码中去掉。如果你去掉了Tick(),同时要确保去掉了构造函数中的关于Tick的这行代码,否则会出问题。
AMyActor::AMyActor()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you do not need it.
PrimaryActorTick.bCanEverTick = true;
}
我们已经新建了类,所以现在我们新增一些属性,让这些属性可以在虚幻引擎中进行设置。让一个属性在引擎中显示很简单,我们使用特殊宏命令,UPROPERTY()。你所需要做的就是使用UPROPERTY(EditAnywhere)宏命令,放在属性定义的上面就可以了。
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
//注意这里默认是私有属性,共有需要加PUBLIC修饰
UPROPERTY(EditAnywhere)
int32 TotalDamage;
...
};
这就是你需要做的全部,这里有更多控制属性的途径,需要在宏命令中添加更多信息。比如,你想要TotalDamage属性显示在与之相关的属性面板,你可以使用“分类”特性,定义如下。
UPROPERTY(EditAnywhere, Category="Damage")
int32 TotalDamage;
当用户看到这个属性时,这个属性已经在名称为“Damage”的面板下方了,并且其他任何属性都可以加上这个分类,使其放在该面板下方,这是一个很好的方法去放置物体的属性。
现在让我们了解下属性关于蓝图的宏命令(下面的属性必须是public修饰)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
int32 TotalDamage;
你可以看到,这个BlueprintReadWrite特定参数使属性可以被蓝图读写,这里有分离的其他选项,BlueprintReadOnly,用这个可以让属性在蓝图中只读(也就是常量),这有一些选项可用去控制属性如何在引擎中展现。想要看更多的关于属性的控制,请看这里。 传送门
在继续下一部分之前,让我们在样本类中添加一对属性,现在已经有了TotalDamage可以在引擎中展示总伤害,让我们更进一步的使伤害发生在每次伤害调用时,下面的代码展示了设计者可以设置的属性,并且其中有1个属性仅可显示,但不能修改。
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
int32 TotalDamage;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
float DamageTimeInSeconds;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient, Category="Damage")
float DamagePerSecond;
...
};
DamageTimeInSeconds 是一个可以再引擎中修改的属性。DamagePerSecond 用来计算伤害的值(看下一部分)。VisibleAnywhere 标志让属性可以在引擎看到,但不能修改。Transient 标志意味着该属性值将不会存储在磁盘;它将派生,非持久。下图是在引擎中的效果。
在构造函数中设置默认值与传统的C++是一样一样的。下面有2个例子是设置默认值的函数写法,他们的效果是一样的,只能用其中的一个哦。
AMyActor::AMyActor()
{
TotalDamage = 200;
DamageTimeInSeconds = 1.f;
}
AMyActor::AMyActor() :
TotalDamage(200),
DamageTimeInSeconds(1.f)
{
}
设置默认值之后,引擎会有同样的改变。
为了支持设计师在每次赋值之后,其他值能够重新装载(重新计算)。这些需要计算的数据是在构造函数之后,你可以添加默认的值,并且挂钩(构造完后自动调用)PostInitProperties()。这个例子关于使用,TotalDamage和DamageTimeInSeconds 来计算每次伤害,当你明智的赋值了之后(除数不能为0之类的),每次伤害会改变。
注意:如果你没有给属性赋默认值,引擎将会自动赋值0或者空指针(指针类的情况)
void AMyActor::PostInitProperties()
{
Super::PostInitProperties();
DamagePerSecond = TotalDamage / DamageTimeInSeconds;
}
同样的面板信息,在添加了PostInitProperties()代码之后,值被计算出来了。
若你用C++开发其他的项目,虚幻有个非常酷的特性会让你大吃一惊。你可以编译你的C++代码的修改而不需要关闭虚幻引擎!这儿有2种方法去做这件事。
1.在引擎任然运行的情况下,像往常一样编译你的项目(生成/重新生成之类的),虚幻引擎会检测最新的编译DLLs和立即重新装载你的改变。
(主意:如果你选择的是Debbuger,你需要先分离?不明白,反正别用Debbuger模式就行啦)
2.或者简单的点击引擎工具条上的“编译”按钮。
你可以在后面的内容中使用这个特性,真的节省时间!
直到现在,我们已经用C++类向导建立了一个简单的游戏类,并且可以在引擎中被设计师赋值。现在让我们一起来看看,设计师如何从小做起(from our humble beginnings)创造一个独一无二的类。
第一件事,以MyActor为父类新建一个蓝图类。注意图片下方的类型名称选择,用MyActor代替AMyActor。这里隐含了命名规范,请把名字起得好一点,别用汉语拼音。
当你选择之后,一个新的、默认的蓝图类已经为你建立好了。重命名为CustomActor1,在Content Browser中可以看到。
这是我们作为设计师建立的第一个自定义蓝图类,第一件事,我们将要改变伤害的默认值。TotalDamage 赋值 300,DamageTimeInSeconds 赋值 2,意味着每秒进行2次伤害。下图展示了现在面板信息。
我们计算出的伤害信息并不是我们所期望的,DamagePerSecond 应该是150而不是200。原因是,我们仅仅在属性被初始化时,计算每次伤害值,并装载。运行时的改变并不属于初始化事件。有个简单解决这个问题的方法,当我们的目标在引擎中改变时。下面代码展示如何与改变操作挂钩。
void AMyActor::PostInitProperties()
{
Super::PostInitProperties();
CalculateValues();
}
void AMyActor::CalculateValues()
{
DamagePerSecond = TotalDamage / DamageTimeInSeconds;
}
#if WITH_EDITOR
void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
CalculateValues();
Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif
关于PostEditChangeProperty() 方法有一点要注意,它包含在特定的#ifdef中,还有把计算部分重构,封装成单独的方法,减少代码量。编译后,DamagePerSecond 属性就是我们所期望的值了。
直至现在,我们已经使变量可以在蓝图中看到,但是在我们深入研究引擎前,还有最后一个话题需要介绍。在新建了游戏系统之间,设计师需能够在蓝图类中,如同C++开发者一样,调用C++类中的方法。让我们开始做第一步,让CalculateValues()方法可以在蓝图类中被调用。让方法在蓝图类中可调用是和属性一样简单,只需要在方法的上一行定义一个宏命令!代码如下:
UFUNCTION(BlueprintCallable, Category="Damage")
void CalculateValues();
UFUNCTION()宏命令句柄可以对反射系统展示此方法,BlueprintCallable 选项使它(CalculateValues()方法)暴露在Blueprints Virtual Machine(蓝图虚拟机),每个在蓝图类中暴露的方法,都需要一个相关联的类别,只有如此,右键单击弹出的内容菜单才能正常的显示方法。下图展示暴露方法后的在蓝图中的效果。
你可以看到,在Damage 类别中可以选择到这个方法。下面的蓝图脚本(连线)展示了,设置了TotalDamage 后,怎么重新计算每次伤害。
这里我们使用我们之前添加过得方法。很多方法都通过UFUNCTION()宏命令暴露给了蓝图,所以设计师可以不用写C++代码。然而,最好的办法是使用C++建立基本的游戏系统,用蓝图展现关键代码被用来自定义游戏行为,或者用C++组合行为。
现在我们的设计师可以调用C++代码(方法)了,让我们探索更多关于跨越C++与蓝图之间的有力的方法。这个途径允许C++代码调用蓝图中定义的方法。我们常常使用这个方法去通知设计者,他们能够响应他们自己觉得合适的一个事件,通常包括效果、视觉碰撞、隐藏/显示物体对象等这些连接操作。下面的代码片段的代码显示了由蓝图实现的功能。
UFUNCTION(BlueprintImplementableEvent, Category="Damage")
void CalledFromCpp();
如同其他的C++方法调用一样,在其背后的逻辑是,虚幻引擎生成了一个基本的C++方法实现,它可以知道如何去调用蓝图虚拟机中的内容。This is commonly referred to as a Thunk.(通常就是咚的一声)。如果蓝图有问题则不会对这个方法提供方法体,这时方法的行为就像C++代码中没有方法体的方法一样:它不做任何事,如果你想提供一个C++默认实现同时允许蓝图覆盖此方法?UFUNCTION()宏命令也有个选项提供此功能。下面代码片段展示如何实现这种需求。
UFUNCTION(BlueprintNativeEvent, Category="Damage")
void CalledFromCpp();
像上面这样写,虚幻引擎任然会咚的一声生成一个方法,使其能够调用蓝图虚拟机。所以怎么去提供默认的方法实现?引擎同样生产了一个新的方法定义“_Implementation()”,你必须提供这个版本的方法,否则你的项目将无法连接。这个实现代码如下。
void AMyActor::CalledFromCpp_Implementation()
{
// Do something cool here
}
像上面这样写,当蓝图方法有问题时,则不会重写这个方法。有件事需要注意,在未来版本的编译工具,自动生成“_Implementation()”定义将要放弃,you'll be expected to explicitly add that to the header.(不明白),在4.7版本,这个生成定义还是可以使用哒。
现在,我们浏览了常规的开发者工作流,开发者与设计师一起去建造游戏特性,现在选择你自己的大冒险。你也可以继续阅读这个文档,更多的了解使用C++引擎,或者跳转至我们的例子(游戏例子),在登录器中有更多手把手的教学内容。