C/C++教程

Introduction to C++ Programming in UE4 章节学习(持续更新)

本文主要是介绍Introduction to C++ Programming in UE4 章节学习(持续更新),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Introduction to C++ Programming in UE4

先是一些入门的小东西。

Tick()

Tick():Actor出现后每一帧都会call它,参数为上一次call它到现在的间隔时间,通常即为帧与帧之间的间隔时间,如果不需要该函数,请丢掉它,能节省一小部分性能,记住也要把Constructor里相关的东西删除指的就是

PrimaryActorTick.bCanEverTick = true;

UPROPERITY()

说明一下,_BlueprintReadOnly_相当于表示该属性为const,关于UPROPERTY宏更多的参数,参考Link。下面举个例。

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;

    ...
};

Transient

UPROPERTY宏的参数,表示短暂的,说明该属性加载时会被填充为0;

PostInitProperties()

当某个属性的初始化值需要由设计师在编辑内设置好其它属性的值,用这些值来产生该初始化值然后赋予,这就需要用到_Super::PostInitProperties()_函数,如下,便能在运行时也能改变那个值。

void AMyActor::PostInitProperties()
{
    Super::PostInitProperties();

    CalculateValues();
}

void AMyActor::CalculateValues()
{
    DamagePerSecond = TotalDamage / DamageTimeInSeconds;
    //DamagePerSecond,即为那个需要其他值来赋予值的属性
}

#ifdef WITH_EDITOR
void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
    CalculateValues();

    Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif

_PostEditChangeProperty()_函数继承自Actor,当所属Actor的属性在编辑器被改变时会触发调用。

Super

是子类对父类的别称。

BlueprintImplementableEvent

UFUNCTION宏的一个参数,用来使函数被认定为是从蓝图中调用的,但蓝图中没有定义该函数,then do nothing。如果想有一个默认的函数体,使用_BlueprintNativeEvent_,并提供额外的默认函数,命名为[FuncionName]_Implementation,举例。

UFUNCION(BlueprintNativeEvent)
void CalledFromCpp();

void CalledFromCpp_Implementation();

//再实现它
void [ClassName]::CalledFromCpp_Implementation()
{
    //do something
}

好的,讲解正式开始,下面介绍四大gameplay class。

UObject

它和UClass搭配,提供了UE最重要的一些services(如下),是引擎最基本的两个类。

  • 映射properties和methods
  • properities的序列化
  • 垃圾回收
  • 通过名字寻找UObject
  • 给propeties配置值
  • properties和methods的网络工作支持

每一个继承自UObject的类的实例,引擎都会自动创建一个包含所有元数据(metadata)的UClass供其使用。

AActor

继承自UObject,要么被直接放置再world当中,要么在运行中通过gameplay系统被加入world中。所有可以被放入level中的对象都继承自该类。它可以被显示消除,也可以通过垃圾回收系统自动消除,还可以通过Lifespan决定它存在多久,然后自动消除。

它的生命周期简单来说就三件事,BeginPlay(), Tick(), EndPlay(),直观一点就是被放入world,做事情,从level里消失。因为操纵一个Actor合理变化十分复杂,引擎提供了一个method,SpawnActor,是UWorld的一个成员。

UActorComponent

即Actor的组件,RootComponent是Actor的成员,根组件嘛,另外,组件和Actor共享Tick。

UStruct

注意,UStruct并不从UObject继承,没有垃圾回收等机制。它的内部应该全部为纯数据。

Unreal Reflection System

gameplay类用一些特殊的宏

来让我们轻易实现映射。下面介绍几种。

Macro Description
UStruct() 让引擎为这个struct产生映射数据
GENERATED_BODY() 为该类产生模板式的constructor

另外,所有产生的映射数据都会存到[ClassName].generated.h文件中,GENERATED_BODY()也在里面。

Object/Actor Iterators

Object Iterators可以将UObject所有的实例包括子类实例全都迭代一遍。如下

for (TObjectIterator<UObject> It; It; It++)
{
	UObject* CurrentObject=*It;
	UE_LOG(LogTemp, Log, TEXT("Found UObject named: %s"), *CurrentObjec->GetName());
}

TObjectIterator<>里也可以指定UObject的子类,如那么将迭代该子类和该子类的子类的所有实例。

注意使用在PIE中使用Object Iterator会导致意外错误。编辑器加载完成后,迭代器会归还所有被放入world的对象实例和编辑器正在使用的实例。

Actor Iterator相当于TObjectIterator,Actor Iterator不会产生上述问题,且只归还被放入current level的对象实例。创建一个Actor Iterator,需要给它指向一个UWorld实例的指针。

APlayerController* MyPC = GetMyPlayerControllerFromSomewhere();
UWorld* World = MyPC->GetWorld();

// Like object iterators, you can provide a specific class to get only objects that are or derive from that class
for (TActorIterator<AEnemy> It(World); It; ++It)
{
    // ...
}

Memory Management and Garbage Collection

说在前头,垃圾回收机制,清除的是不再被引用(被指针指向)或已被显式标记为即将被回收的内存。

你创建了一个类A,在类中定义一些成员指针变量,其类型是B,它会指向一块内存,该指针便是对这块内存的一个reference,垃圾回收便是把这指针所指向的内存回收,并把指针设置为nullptr。

至于你所建的类A,在其它类中,可能会有A类型的指针,它申请一块内存,至于这块内存是否在垃圾回收系统范畴,就看你建的这个类A,是否符合规定。

所以,你在你所建立的类里应该讨论的是成员变量,讨论类本身在类内是没有意义的。

UObject

UE使用映射系统来执行垃圾回收,需要垃圾回收机制的类需是UObject或其子类。

垃圾回收有一个概念叫做root set,即一个包含一些对象的列表,回收系统保证不会回收这些对象。把这个列表想象成一棵树,树所触及不到的实例对象,全都当垃圾回收了。垃圾回收会一轮一轮在固定时间进行。

UObject不会被当作垃圾回收的条件有三种:

  1. UObject对象被加入到root set上(调用AddRoot函数)。
  2. 直接或者间接被root set 里的对象引用(如UPROPERTY宏修饰的UObject成员变量 注:UObject放在UPROPERTY宏修饰的TArray、TMap中也可以)
  3. 直接或间接被存活的FGCObject对象引用(后面会讲)

举例

void CreateDoomedObject()
{
    MyGCType* DoomedObject = NewObject<MyGCType>();
}

这里的DoomedOjbect指针就没有被UPROPERTY宏修饰(或在被UPROPERTY宏修饰的UE容器类里),即root set触及不到,会被垃圾回收消除。

Acotr

除了level关闭,Actor一般不会被垃圾回收,它们产生后,一般需要手动调用消除函数(只是从root set中移除,还需等待下轮垃圾回收),这之后它们会立马被排除在world外,然后垃圾回收系统就能检测到异端了,会在下一轮把它回收掉。举例

UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

public:
    UPROPERTY()
    MyGCType* SafeObject;

    MyGCType* DoomedObject;

    AMyActor(const FObjectInitializer& ObjectInitializer)
        : Super(ObjectInitializer)
    {
        SafeObject = NewObject<MyGCType>();
        DoomedObject = NewObject<MyGCType>();
    }
};

void SpawnMyActor(UWorld* World, FVector Location, FRotator Rotation)
{
    World->SpawnActor<AMyActor>(Location, Rotation);
}

当我们调用SpawnMyActor函数时,MyActor会产生在world里。SafeObject前有UPROPERTY宏修,但DoomedObject并没有,它会被垃圾回收机制检测到并消除,留下一个dangling(空悬)指针。(解释一下,野指针是没有初始化的指针,根本不知道指的啥;空悬指针是指那种生命周期比所指对象还长的指针,在所指对象被回收后,它仍指向那块内存,若系统给那块内存分配了东西,会有意外发生)

但注意,当一块存储UObject的内存被回收之后,所有被UPROPERTY宏修饰并指向这块内存的指针都会被设置为nullptr,这样就消除了空悬指针,这也使得你在使用这些指针的时候,要先确认一下是否为nullptr,因为还有一点,手动调用函数消除实际上是把该指针所指对象从root set里移除,并等待下一轮的垃圾回收,用IsPendingKill检验是否在等待

if (MyActor->SafeObject != nullptr)
{
    // Use SafeObject
}

UStruct

没有垃圾回收机制,非要使用它的动态实例,则需要智能指针的登场。

Non-UObject References

普通的c++类(非继承自UObject)需继承自FGCObject类,并重载AddReferenceObject()就也能添加对其的reference且不会被垃圾回收系统强制回收。需要说明的是,垃圾回收系统是一种无差别攻击系统,不在名单里的统统消灭。举例

class FMyNormalClass : public FGCObject
{
public:
    UObject* SafeObject;

    FMyNormalClass(UObject* Object)
        : SafeObject(Object)
    {
    }

    void AddReferencedObjects(FReferenceCollector& Collector) override
    {
        Collector.AddReferencedObject(SafeObject);
    }
};

我们用FReferenceCollector来手动添加对该UObject的hard reference,而在对象被删除且destructor正常运行时,它会自动消除所有reference。

更详细的剖析

说一下,垃圾回收系统有两套,分别处理UObject和非UObject,想要创建的类能够被加入垃圾回收系统,只要让继承自UObject类的变量套上UPROPERTY的宏就可以了,因为这样就是被root set里的对象引用了,而继承自非UObject类的变量——则需要干以下几件事。

  • 让这个类一开始写的时候就继承FGCObject类。

  • 如果成员变量中有UObject类,在复写的AddReferencedObjects()方法中,将引用的UObject变量加入到Collector中即可。

  • 如果成员变量中有 非UObject类,则需要将其声明为UE自定义的智能指针。\

UE Type

Class

特殊的命名规则,给予特殊的便利与保护。

  • 继承自AActor,名前会加上A
  • 继承自UObject,名前会加上U
  • Enums类型前会加上E
  • Interface类型前会加上I
  • Template类型前会加上T
  • 继承自SWidget (Slate UI),名前会加上S
  • 其它都会加上F

Number

整数:

  • int8/uint8: 8-bit signed/unsigned integer
  • int16/uint16: 16-bit signed/unsigned integer
  • int32/uint32: 32-bit signed/unsigned integer
  • int64/uint64: 64-bit signed/unsigned integer

浮点数:

​ float (32-bit) and double (64-bit)

UE中还有一个Template,TNumericLimits,可以查找一类型数字的最大最小范围

String

UE提供了很多,但这一篇里文档没讲。

FString

是一种mutable string,用TEXT(" ")创建,日志输出一般都是用它。

FText

与FString类似,但它是localized text,两种方式创建,一是用NSLOCTEXT宏,需要a namespace, key, and a value三个参数;二是LOCTEXT宏,只需namespace,value两个参数,举例

//第一种
FText MyText = NSLOCTEXT("Game UI", "Health Warning Message", "Low Health!")

//第二种
// In GameUI.cpp
#define LOCTEXT_NAMESPACE "Game UI"

//...
FText MyText = LOCTEXT("Health Warning Message", "Low Health!")
//...

#undef LOCTEXT_NAMESPACE
// End of file

FName

它主要用来存储十分常用的字符串,如果有多个对象引用同一个字符串,FName能使用较小的空间存储索引来映射(map)到给定字符串,它更快也是因为引擎能够检查其索引值来确认其是否匹配,而无须检查每一个字符是否相同。

TCHAR

TCHAR类型是独立于所用字符集存储字符,考虑到的是字符集或许会因平台而异。实际上,UE的字符串使用 TCHAR 数组来存储 UTF-16 编码的数据。可以使用返回TCHAR的overloaded dereference operator来访问the raw data。

某些函数要用它,例如 FString::Printf()

FString Str1 = TEXT("World");
int32 Val1 = 123;
FString Str2 = FString::Printf(TEXT("Hello, %s! You have %i points."), *Str1, Val1);

"%s" 字符串格式说明符要的是TCHAR,一般就给它_*FString_。

FChar类提供一系列static utility function处理TCHAR的单个字符,举例

TCHAR Upper('A');
TCHAR Lower = FChar::ToLower(Upper); // 'a'

接下来介绍一些Container。

TArray

类似于std::vector,但有更多功能,下面是一些普通的操作。

TArray<AActor*> ActorArray = GetActorArrayFromSomewhere();

// 看有多少elements
int32 ArraySize = ActorArray.Num();

// 第一个元素的索引为0
int32 Index = 0;

// 检索一个值。
AActor* FirstActor = ActorArray[Index];

// 在TArray末尾添加element
AActor* NewActor = GetNewActor();
ActorArray.Add(NewActor);

// 添加一个TArray里本不存在的element,若存在,则不添加
ActorArray.AddUnique(NewActor); 

// 将TArray里的所有NewActor移除
ActorArray.Remove(NewActor);

// 移除索引处的值,并将后面的所有值往前挪一位,即不留空位
ActorArray.RemoveAt(Index);

// 移除索引处的值,与上不同,会将TArray里最后一个值挪到空缺处
ActorArray.RemoveAtSwap(Index);

// 清空
ActorArray.Empty();

另外,像之前说的一样,被UPROPERTY宏修饰的TArray的UObject成员拥有垃圾回收的权限。

UCLASS()
class UMyClass : UObject
{
    GENERATED_BODY();

    // ...

    UPROPERTY()
    AActor* GarbageCollectedActor;

    UPROPERTY()
    TArray<AActor*> GarbageCollectedArray;

    TArray<AActor*> AnotherGarbageCollectedArray;
    
    // 是吧,这些也都是指针
};

TMap

类似于std::map,具体方法文档在该处给了个实例,这里截取一小部分,简单明了。

 TMap<FIntPoint, FPiece> Data;
 
 Data.Contains(Position);
 
 FPiece Value = Data[Position];
 
 Data.Add(Position, NewPiece);
 
 Data.Remove(OldPosition);
 
 Data.Empty();

TSet

类似于std::set,直接上例子,也是简单明了

TSet<AActor*> ActorSet = GetActorSetFromSomewhere();

int32 Size = ActorSet.Num();

AActor* NewActor = GetNewActor();
ActorSet.Add(NewActor);

if (ActorSet.Contains(NewActor))
{
    // ...
}

ActorSet.Remove(NewActor);

ActorSet.Empty();

// 创造一个包含TSet里所有elements的TArray
TArray<AActor*> ActorArrayFromSet = ActorSet.Array();

Container Iterator

直接上例子。

void RemoveDeadEnemies(TSet<AEnemy*>& EnemySet)
{
    for (auto EnemyIterator = EnemySet.CreateIterator(); EnemyIterator; ++EnemyIterator)
    {
        AEnemy* Enemy = *EnemyIterator;
        if (Enemy.Health == 0)
        {
            // RemoveCurrent()是TSet和TMap的方法
            EnemyIterator.RemoveCurrent();
        }
    }
}
// 退回到前一个element
--EnemyIterator;

// 前进或后退offset个element
EnemyIterator += Offset;
EnemyIterator -= Offset;

// 获得迭代器现在的索引
int32 Index = EnemyIterator.GetIndex();

// 让迭代器回到第一个element
EnemyIterator.Reset();

For-Loop

下面是for循环适应于TArray,TSet,TMap的用法。

// TArray
TArray<AActor*> ActorArray = GetArrayFromSomewhere();
for (AActor* OneActor : ActorArray)
{
    // ...
}

// TSet - Same as TArray
TSet<AActor*> ActorSet = GetSetFromSomewhere();
for (AActor* UniqueActor : ActorSet)
{
    // ...
}

// TMap - Iterator returns a key-value pair
TMap<FName, AActor*> NameToActorMap = GetMapFromSomewhere();
for (auto& KVP : NameToActorMap)
{
    FName Name = KVP.Key;
    AActor* Actor = KVP.Value;

    // ...
}

从上面的代码中可以看到auto不会自动识别指针和引用,需要手动添加 * 或 & 。

Using Your Own Types with TSet/TMap (Hash Functions)

TSet和TMap内部都需要哈希函数,大部分UE types都已经定义了专属的哈希函数,如果你自定义的类需要用在TSet或作为key用在TMap里,需要提供一个参数为你定义的这个类的指针或引用,返回值为uint32,这个返回值需是你的类独有代号,举例。

class FMyClass
{
    uint32 ExampleProperty1;
    uint32 ExampleProperty2;

    // Hash Function作为friend
    friend uint32 GetTypeHash(const FMyClass& MyClass)
    {
        // HashCombine(),内部函数,结合两个哈希值
        uint32 HashCode = HashCombine(MyClass.ExampleProperty1, MyClass.ExampleProperty2);
        return HashCode;
    }

    // 为了演示证明,使用两个相同类型的对象
    // should always return the same hash code.
    bool operator==(const FMyClass& LHS, const FMyClass& RHS)
    {
        return LHS.ExampleProperty1 == RHS.ExampleProperty1
            && LHS.ExampleProperty2 == RHS.ExampleProperty2;
    }
};

如果用指针作为key,即TSet<ClassName*>,那么上面相应位置应该这么用:

uint32 GetTypeHash(const ClassName* ValueName)。

Asserts

首先回顾一下c++中关于assert的知识:

assert,意思是断言,需包含头文件assert.h。assert其实是宏定义,而非函数,用在程序调试阶段检查错误,判断expression是否为假,为假时,会调用abort报警。

void assert(int expression);

// 举例
assert(("a必须大于10", a > 10));
// 或者
assert(a > 10 && "a必须大于10");	

// 输出结果样式如下
Assertion failed: expression, file [FileName], line [num].

assert只有在Debug中才有效,如果编译为Release则被忽略。
如果不想使用它,可以在#include语句之前,插入#define NDEBUG,就可以禁用assert了。

assert通常用来检查三种情况,指针是否为空、除数发是否为零、函数是否递归运行,当然代码要求的其他重要假设也可能会用到,但缺点是效率低。

某些情况下,assert 能在真正的崩溃 (crash)发生前,发现造成延迟崩溃的bug,像是删掉在之后的Tick中会用到的对象,帮助找到崩溃的源头,当然其最关键的feature,像之前说的一样,不会出现在shipping code中。

好,回到UE。

UE提供assert的三种等价体系, checkverifyensure,三个有细微差别,但主要作用相同,都声明于 AssertionMacros.h 头文件中。(注意这些都是体系,每个里面又很多可用的宏)

Check

check体系是三个当中最接近assert的,当在参数里发现为false的表达式时,立马停止运行,默认也不会在shipping版本中运行。下面是check体系的可用宏。

Macro Parameters Behavior
check / checkSlow Expression Expression为false时停止运行
checkf / checkfSlow Expression, FormattedText, ... Expression为false时停止运行,并在日志中输出FormattedText
checkCode Code 在do-while循环中执行Code,while条件硬性规定为false,即只运行一次,主要用来准备其它Check所需要的信息
checkNoEntry (none) 一旦触及,停止运行,类似于check(false),但主要倾向于说明程序不能走向这里
checkNoReentry (none) 第二次触及这里,停止运行,就是只允许紧接其后的代码运行一次
checkNoRecursion (none) 第二次到这儿如果没有离开当前作用域,停止运行
unimplemented (none) 一旦触及,停止运行,类似于check(false),主要用于设计上希望被override且不会被调用的虚函数

这些宏当中,除了以Slow结尾的只在Debug中运行,其余的在Debug,Development中均可运行。

UE的Check体系中保留有一个USE_CHECKS_IN_SHIPPING的宏定义,用以标记Check检查可在所有版本执行,其默认值为0,主要用于怀疑check中的代码在修改值,或者发现仅存于发布版本的bug。

// 这个函数的传入参数JumpTarget如果是nullptr,那么运行会停止
void AMyActor::CalculateJumpVelocity(AActor* JumpTarget, FVector& JumpVelocity)
{
    check(JumpTarget != nullptr);
    // 计算速度需要JumpTarget,这里保证它不是nullptr
}
// HasCycle()检查MyLinkedList中有没有闭环,因为检查闭环很费时间,我们只在Debug中检查
checkfSlow(!MyLinkedList.HasCycle(), TEXT("Found a cycle in the list!"));
// (Walk through the list, running some code on each element.)
// IsEverythingOk()没有额外的作用,就是看有没有致命性的错误
// If this happens, terminate with a fatal error.
// 因为这段没有其它作用且只是诊断检查,所以无需在shipping版本中运行
checkCode(
    if (!IsEverythingOK())
    {
        UE_LOG(LogUObjectGlobals, Fatal, TEXT("Something is wrong with %s! Terminating."), *GetFullName());
    }
);
// 如果我们有一个新的Shape Type却没加入这段switch中,就会停止运行
switch (MyShape)
{
    case EShapes::S_Circle:
        // (Handle circles.)
        break;
    case EShapes::S_Square:
        // (Handle squares.)
        break;
    default:
        // 不应该有没说明的Shape Type,所以此路不通
        checkNoEntry();
        break;
}

Verify

和Check体系差不多,但它可以在Check被禁掉的版本中仍计算表达式的值,注意这并不会触发运行停止,所以当表达式需要在诊断检查之外独立运行时,才使用该宏。

举个例子:如果要停止运行并检查(即断言检查)一个函数,假设函数返回bool值并以此作为断言参数,此时check和verify的行为一致,而在shipping版本中,它们开始有差异,verify在发行版本中会忽略函数的返回值(即不进行断言检查),但仍然会执行函数,而check则不会执行。

就是说,如果需要断言检查的参数表达式始终执行,则使用verify体系。

Macro Parameters Behavior
verify / verifySlow Expression Expression为false时,停止运行
verifyf / verifyfSlow Expressin, FormattedText, ... Expression为false时,停止运行,并在日志中输出FormattedText

同Check体系一样,这些宏当中,除了以Slow结尾的只在Debug中运行,其余的在Debug,Development中均可运行,而且如上面所说,在所有版本中,包括shipping版本,Verify体系都会计算表达式的值。

同Check体系一样,Verify体系留有一个USE_CHECKS_IN_SHIPPING的宏定义,默认为1,如果overide它,那么在除了1的其它所有情况下,Verify体系都只会计算表达式的值,而不会停止运行。

另外verifyfSlow宏貌似在某个版本中被删除了。

// 设置Mesh的值并确认是否为null,如果是,停止运行
// 这里使用verify的原因是不管怎样,Mesh都需要设置一个值
verify((Mesh = GetRenderMesh()) != nullptr);

Ensure

类似于Verify体系,Ensure和Verify一样始终(在shipping中也如此)计算表达式的值,但不同的是,它不会停止运行,而是通知crash reporter,程序接着run。

需要特别注意的是,为了防止crash reporter死命报告错误,一次引擎或编辑器会话中触发 ensure 断言只会报告一次,如果想总是报告,用带有Always的Ensure宏。

Macro Parameters Behavior
ensure Expression Expression为false时,通知crash reporter
ensureMsgf Expression, FormattedText, ... Expression为false时,通知crash reporter,并在日志中输出FormattedText
ensureAlways Expression 带有Always
ensureAlwaysMsgf Expression, FormattedText, ... 带有Always

在所有版本中都会计算表达式的值,但只会在Debug, Development, Test, and Shipping Editor builds版本中通知crash reporter。

// 这段代码可能会在shipping版本中有一个细小的错误,小到无需为它停止程序,就是想到也许已经修好了它,来验证一下
void AMyActor::Tick(float DeltaSeconds)
{
    Super::Tick(DeltaSeconds);
    // 确保bWasInitialized是true,不是的话就会在log中输出信息
    if (ensureMsgf(bWasInitialized, TEXT("%s ran Tick() with bWasInitialized == false"), *GetActorLabel()))
    {
        // (Do something that requires a properly-initialized AMyActor.)
    }
}

(说一点,shipping editor版本被删掉了)

Programming Basics

Game-Controlled Cameras

讲解如何控制Cameras,首先把Camera扔到level里。

创建一个继承自AActor的c++类,命名为CameraDirector。

// 在.h文件中添加以下成员变量,加到第二个个public里,为什么有两个public的区分(???)


UPROPERTY(EditAnywhere)
AActor* CameraOne;

UPROPERTY(EditAnywhere)
AActor* CameraTwo;

float TimeToNextCameraChange;


// 然后在ACameraDirector::Tick函数里添加以下代码

const float TimeBetweenCameraChanges = 2.0f;
const float SmoothBlendTime = 0.75f;
TimeToNextCameraChange -= DeltaTime;
if (TimeToNextCameraChange <= 0.0f)
{
    TimeToNextCameraChange += TimeBetweenCameraChanges;

    // 获取自己控制的actor,这里的重点就是获取自己的APlayerController
    // APlayerController是一个类,为什么这个类能在这里用,哈,你引入的头文件里面也引入了其它头文件,错综复杂,最终绝对引入了APlayerController.h,至于UGameplayStatics类,它就在GameplayStatics.h里
    APlayerController* OurPlayerController = UGameplayStatics::GetPlayerController(this, 0);
    if (OurPlayerController)
    {
        // 开始换Camera了
        if ((OurPlayerController->GetViewTarget() != CameraOne) && (CameraOne != nullptr))
        {
            // Cut instantly to camera one.
            OurPlayerController->SetViewTarget(CameraOne);
        }
        else if ((OurPlayerController->GetViewTarget() != CameraTwo) && (CameraTwo != nullptr))
        {
            // Blend smoothly to camera two.
            OurPlayerController->SetViewTargetWithBlend(CameraTwo, SmoothBlendTime);
        }
    }
}
// 这段代码会让我们每三秒切换一次Camera

接下来在Editor的C++文件夹里找到你的CameraDirector类,扔进level里,再在Detial面板里设置CameraOne和CameraTwo,其实设置成不是CameraActor的类也行(阿这)。

文档里的练习跟着做一下。Exercise

Player Input and Pawns

用Pawn类来接受player输入。

创建一个继承自Pawn的c++类,就命名为MyPawn。

// 在Constructor里添加以下代码,先让它能自动接受输入信息,并将它设置成由第一位player控制
AutoPossessPlayer = EAutoReceiveInput::Player0;

// 在.h文件里引入以下头文件
#include "Kismet/GameplayStatics.h"
#include "Camera/CameraComponent.h"
// 在头文件里创建Component,如下
UPROPERTY(EditAnywhere)
USceneComponent* OurVisibleComponent;
UPROPERTY(EditAnywhere)
UCameraComponent* OurCamera;


// 回到Constructor里,添加以下代码

// 创建一个虚的root component,相当于一个pivot point,这里的RootComponent是Actor的成员变量,即再Actor.h里定义的
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));

// 创建一个Camera Component,并给之前声明的OurVisibleComponent赋值
// 这里的CreateDefaultSubobject的返回值就是USenceComponent及其子类(写在<>里的),应该就是用来创建组件的
OurCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("OurCamera"));
OurVisibleComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("OurVisibleComponent"));

// 将Camera和VisibleComponent连到root component上,并转一下Camera
// 这里的Relative位置应该就是根组件的相对位置,也就是这虚假的root component所在的pivot point的相对位置
OurCamera->SetupAttachment(RootComponent);
OurCamera->SetRelativeLocation(FVector(-250.0f, 0.0f, 250.0f));
OurCamera->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
OurVisibleComponent->SetupAttachment(RootComponent);

记住把MyPawnActor扔到level里后还需选中它然后选择一个StaticMesh给它用,先选择这个组件再把StaticMesh拖过去,如图

1

有两种映射输入的类型:Action和Axis(轴)。

Action Mapping:适用于Yes/No的输入,像是按下鼠标或手柄,按下、松开、双击或短按,长按都可以用这种映射方式。

Axis Mapping:适用于那种连续的输入,像是一直推着手柄操纵杆,或是鼠标光标的位置,即使没有发生改变,它们仍会每一帧地报告自己的值。

尽管设置银蛇输入可以在代码中进行,但一般我们在Editor里弄这玩意。

在Project Setting->Engine->Input自己去设置吧,很简单,这里设置的是你Action Mapping的按键,Axis Mapping的按键和每一帧会产生的值。

下面就在代码中使用这些值。

// 首先再头文件里声明这些函数和变量
//Input functions
void Move_XAxis(float AxisValue);
void Move_YAxis(float AxisValue);
void StartGrowing();
void StopGrowing();

//Input variables
FVector CurrentVelocity;
bool bGrowing;

// 在.cpp文件里实现它们

void AMyPawn::Move_XAxis(float AxisValue)
{
    // 每秒向前或向后移动100个单元,这里的单元应该指AxisValue,即你在Editor里设置的数字
    CurrentVelocity.X = FMath::Clamp(AxisValue, -1.0f, 1.0f) * 100.0f;
}

void AMyPawn::Move_YAxis(float AxisValue)
{
    // 每秒向前或向后移动100个单元,这里的单元应该指AxisValue,即你在Editor里设置的数字
    CurrentVelocity.Y = FMath::Clamp(AxisValue, -1.0f, 1.0f) * 100.0f;
}
// FMath::Clamp()函数能限定值在一定范围内,如果有多个键能对该值造成影响,可以防止同时按下这几个键时,该值偏离过大

void AMyPawn::StartGrowing()
{
    bGrowing = true;
}

void AMyPawn::StopGrowing()
{
    bGrowing = false;
}

// 下面代码添加到AMyPawn::SetupPlayerInputComponent里去,就是将按键所传达的值经上面几个函数转化后,与InputComponent绑定

// 绑定Action Mapping,实质是判断你“Grow”这个键有没有按下
InputComponent->BindAction("Grow", IE_Pressed, this, &AMyPawn::StartGrowing);
InputComponent->BindAction("Grow", IE_Released, this, &AMyPawn::StopGrowing);

// 绑定Axis Mapping,实质是判断你“Move_X/Y”这个键有没有按下
InputComponent->BindAxis("MoveX", this, &AMyPawn::Move_XAxis);
InputComponent->BindAxis("MoveY", this, &AMyPawn::Move_YAxis);


上面都是绑定,下面就是绑定后能用按下按键所传入的值做些什么
// 基与“Grow” Action放大或缩小
{
    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));
}

// 基与“Move_X/Y”Axis控制移动
{
    if (!CurrentVelocity.IsZero())
    {
        FVector NewLocation = GetActorLocation() + (CurrentVelocity * DeltaTime);
        SetActorLocation(NewLocation);
    }
}

Components and Collision

介绍如何用Components让Pawn于Collision等等交互。

一样,创建一个继承自Pawn的c++类,命名为CollidingPawn。

// 在.h文件里加入以下成员变量


// 在.CPP文件里引入以下头文件,都是要用到的,也就实现两个东西,基础的物理碰撞,和一点小小的里子特效(摩擦起火)
#include "UObject/ConstructorHelpers.h"
#include "Particles/ParticleSystemComponent.h"
#include "Components/SphereComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"


这篇关于Introduction to C++ Programming in UE4 章节学习(持续更新)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!