前言:
笔者目前在校本科大三,目标方向是人工智能、计算机视觉。上一个OpenCV学习笔记专栏已完结,在学习完OpenCV后,我继续学习C++,并用纯C++做UE4项目的方式继续提升自己的水平。
梁迪老师的水平非常高,他的课程本来也无需笔记:课程本身即为最好的笔记。但由于我天赋有限,还是边看边记,以防遗忘——知识点太多,步骤太繁杂了。在学习过程中,我也偶有思考,思索为什么某个方法老师要这样做。所以,一是为了记录,二是为了分享,才有了这个专栏。
内容方面,由于我在开启这个专栏时,此项目已经做完很多了。所以,前期的一些大篇幅叙述的知识,可能在后期应用中一带而过。以及,前期的一些知识,后期会重新剖析,并加上我的个人理解。
另外,若有学术交流/学业交流意愿,可以邮件联系1246210283@qq.com,希望一齐进步。
本篇学习内容:
53.敌人攻击AI
54.游戏暂停菜单
53.敌人攻击AI
(1)动作修改
敌人的攻击动作中,有些动作是伴随移动的。而UE4提供的Root Motion下的EnableRootMotion并不能在固定根骨骼的同时不固定它的缩放等属性,所以需要在代码里修改。
if (Montage_IsPlaying(AnimAttack_III))
{
CurrentPlayTime += DeltaSeconds;
CurrentPlayTime = FMath::Clamp<float>(CurrentPlayTime, 0.f, AnimAttackSeq_III->GetPlayLength());
FTransform OutputTrans;
AnimAttackSeq_III->GetBoneTransform(OutputTrans, 0.f, CurrentPlayTime, true);
RootBonePos = FVector(OutputTrans.GetLocation().X, StartYPos, OutputTrans.GetLocation().Z);
}
(2)在动作蓝图里设置动作
(3)创建文件TaskAttackSwitch、TaskAttackDash、TaskAttackFollow、TaskAttackNormal、TaskAttackPursuit、TaskRotate,均继承于TaskBase
(4)写TaskAttackSwitch、TaskAttackDash、TaskAttackNormal
主要都是写执行函数。但是TaskAttackDash要额外多一些:
//重写执行函数
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
//重写终止任务函数 跳出此任务,到上一个父节点时执行
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
protected:
//动作结束后事件
void OnAnimationTimerDone();
protected:
UPROPERTY(EditAnywhere, Category = "Blackboard")
struct FBlackboardKeySelector Destination;
UPROPERTY(EditAnywhere, Category = "Blackboard")
struct FBlackboardKeySelector WaitTime;
//攻击动作结束后的委托
FTimerHandle TimerHandle;
注:实际上通过动画的时长来判定运行时间是不合理的,因为运行游戏时帧数低就会导致运行时间很长。最好的方法是在动画播放的最后一帧添加一个通知,让这个通知来告诉行为树:这个动画已经播放完了。
EBTNodeResult::Type USlAiEnemyTaskAttackDash::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory)
{
//如果初始化敌人参数不成功,直接返回失败
if (!InitEnemyElement(OwnerComp)) return EBTNodeResult::Failed;
//播放冲刺动画 实际上通过动画的时长来判定运行时间是不合理的,因为运行游戏时帧数低就会导致运行时间很长。
//最好的方法是在动画播放的最后一帧添加一个通知,让这个通知来告诉行为树:这个动画已经播放完了。
float AttackDuration = SECharacter->PlayAttackAction(EEnemyAttackType::EA_Dash);
//范围是0
const float ChaseRadius = 5.f;
//获取玩家到敌人之间的单位向量
FVector SPToSE = SEController->GetPlayerLocation() - SECharacter->GetActorLocation();
SPToSE.Normalize();
//探索起点:玩家位置减去与敌人之间距离的一点点
const FVector ChaseOrigin = SEController->GetPlayerLocation() - 20.f * SPToSE;
//保存随机位置
FVector DesLoc(0.f);
//使用导航系统获取随机点
UNavigationSystem::K2_GetRandomReachablePointInRadius(SEController, ChaseOrigin, DesLoc, ChaseRadius);
//角色速度
float Speed = (FVector::Distance(SECharacter->GetActorLocation(), DesLoc)) / AttackDuration + 30.f;
//修改敌人的速度
SECharacter->SetMaxSpeed(Speed);
//修改参数
OwnerComp.GetBlackboardComponent()->SetValueAsVector(Destination.SelectedKeyName, DesLoc);
OwnerComp.GetBlackboardComponent()->SetValueAsFloat(WaitTime.SelectedKeyName, AttackDuration);
//添加事件委托 修改速度为300
FTimerDelegate TimerDele = FTimerDelegate::CreateUObject(this, &USlAiEnemyTaskAttackDash::OnAnimationTimerDone);
//注册到事件管理器
SEController->GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDele, AttackDuration, false);
return EBTNodeResult::Succeeded;
}
EBTNodeResult::Type USlAiEnemyTaskAttackDash::AbortTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory)
{
//如果初始化敌人参数不成功,或者事件句柄没有激活,直接返回失败
if (!InitEnemyElement(OwnerComp) || !TimerHandle.IsValid()) return EBTNodeResult::Aborted;
//卸载事件委托
SEController->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
return EBTNodeResult::Aborted;
}
void USlAiEnemyTaskAttackDash::OnAnimationTimerDone()
{
//重新设置速度为300
if (SECharacter)SECharacter->SetMaxSpeed(300.f);
}
(5)写TaskAttackPursuit,基本逻辑和TaskAttackDash类似
(6)写TaskAttackFollow和TaskAttackRotate
(7)设置行为树
注:UE4自带的Rotate to face BB entry 是很生硬的旋转,所以我们在TaskPursuit下使用TaskRotate写的旋转方法:在EnemyCharacter的帧函数下用插值的方式旋转。
注:Time Limit + Move To 可以用Service里的帧函数实现。但是那个帧函数的帧率是不稳定的,所以这里还是用Time Limit + Move To。
54.游戏暂停菜单
(1)根组件是SBox,其下创建一个SVerticalBox,再创建
TArray<TSharedPtr<SCompoundWidget>> MenuItemList;
TArray<TSharedPtr<SCompoundWidget>> OptionItemList;
保存组件信息。
(2)用一个函数InitializeWidget()来初始化MenuItemList和OptionItemList。初始化之后,给VertBox添加MenuItemList的组件。
(3)写4个按钮对应的事件。有些事件需要重置菜单按钮。如:
FReply SSlAiGameMenuWidget::GoBackEvent()
{
//清空VertBox
VertBox->ClearChildren();
//填充菜单按钮
for (TArray<TSharedPtr<SCompoundWidget>>::TIterator It(MenuItemList); It; ++It) {
VertBox->AddSlot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(10.f)
.FillHeight(1.f)
[
(*It)->AsShared()
];
}
//设置RootBox高度
RootBox->SetHeightOverride(400.f);
return FReply::Handled();
}
(4)准备ChangeCulture和ChangeVolume,绑定到SNew的SSlAiGameOptionWidget下。