Giter Site home page Giter Site logo

Comments (14)

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

2. Realistic Landscapes 实景景观

源码版UE包含 Quixel Bridge 和虚幻商城

image

在静态网格体编辑界面,按住ALT,使用右键拖动,可以缩放网格体视角
按住ALT,使用左键拖动,可以围绕网格体旋转视角
按住右键拖动,围绕世界旋转视角

在场景中,按住shift同时移动物体,可使相机跟随移动
G键,然后F11,进入沉浸浏览

开放世界关卡 open world / empty open world

image

新建 空白开放世界 关卡
默认包含:
世界数据层 WorldDataLayers
世界分区小地图 WorldPartitionMiniMap

开启 世界分区 面板
image
image
世界分区 显示当前位置

加载世界分区指定部分
左键选择范围,右键-从选择加载区域
image

光照

sky atmosphere 天空大气旨在创建类似地球的大气,可以散射光线,就像真实的大气一样

定向光模拟无限远的光源。这意味着投射在世界上所有物体上的阴影都是彼此平行的。
使用定向光源来模拟太阳。

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

3. 向量、旋转器和三角函数 Vectors, Rotators, and Trigonometry

坐标 coordinates

一维坐标 coordinates

image

标量scalar/数字量:距离0点的值
image

二维坐标

image

三维坐标

image

向量 Vectors

具有起点和终点的箭头称为矢量/向量。
向量包含两个信息:方向和大小。
向量具有明确定义的方向,因为我们知道它的起始位置和结束位置。
向量也有一个明确定义的大小,因为我们知道它有多长。
image
2D 矢量具有 X 和 Y 分量
向量仅由其方向和大小定义

两个相同的向量
image

image

向量减法公式

向量减法公式用于计算两个向量相减的结果。给定两个向量 $( \mathbf{A} = (a_1, a_2, a_3, \ldots, a_n) )$$( \mathbf{B} = (b_1, b_2, b_3, \ldots, b_n) )$,它们的减法 $( \mathbf{C} = \mathbf{A} - \mathbf{B} )$定义为:

$$[ \mathbf{C} = \mathbf{A} - \mathbf{B} = (a_1 - b_1, a_2 - b_2, a_3 - b_3, \ldots, a_n - b_n) ]$$

其中,

$$( \mathbf{C} )$$

是一个新的向量,其每个分量是 $( \mathbf{A} )$ 对应分量与 $( \mathbf{B} )$ 对应分量之差。

举例

假设有两个三维向量 $( \mathbf{A} = (1, 2, 3) )$$( \mathbf{B} = (4, 5, 6) )$ ,它们的减法结果 $( \mathbf{C} = \mathbf{A} - \mathbf{B} )$ 为:

$$[ \mathbf{C} = (1 - 4, 2 - 5, 3 - 6) = (-3, -3, -3) ]$$

向量减法在物理、工程学和数学中都有广泛应用,例如在计算物体的位移时。

向量 乘法/加减法

image
image
image

image
image

vector magnitude

向量的幅度(magnitude),又称向量的长度或范数,是指向量从原点到其端点的直线距离。在二维或三维空间中,向量幅度可以通过勾股定理来计算。

$$对于一个二维向量 \( \mathbf{v} = (x, y) \),其幅度计算公式为: \[ |\mathbf{v}| = \sqrt{x^2 + y^2} \] 对于一个三维向量 \( \mathbf{v} = (x, y, z) \),其幅度计算公式为: \[ |\mathbf{v}| = \sqrt{x^2 + y^2 + z^2} \] 这里,\( x, y, \) 和 \( z \) 分别是向量在各个坐标轴上的分量。向量的幅度总是非负的,代表着向量的“大小”。在物理学和工程学中,了解向量的幅度非常重要,因为它表示了例如速度、力等物理量的大小。$$

image

Vector Normalization 向量归一化

image

向量归一化(Vector Normalization)是指将一个向量转换为长度(或幅度)为 1 的向量的过程,同时保留其方向不变。这种归一化后的向量通常称为单位向量。

$$对于给定的非零向量 \( \mathbf{v} = (x, y, z) \),其归一化向量 \( \mathbf{v}_{\text{norm}} \) 计算公式为: \[ \mathbf{v}_{\text{norm}} = \frac{\mathbf{v}}{|\mathbf{v}|} \] 其中 \( |\mathbf{v}| \) 是向量 \( \mathbf{v} \) 的幅度(长度),计算为 \( \sqrt{x^2 + y^2 + z^2} \)。$$

归一化后的向量有着相同的方向,但其长度变为 1。向量归一化在计算机图形学、物理学、机器学习等领域非常重要,尤其是在需要处理方向而不是长度的场景中。

zuo'shou左手坐标系 与 右手坐标系

image

虚幻引擎使用左手坐标系
image

在虚幻引擎(Unreal Engine)中,Pitch、Yaw 和 Roll 是用来描述对象在三维空间中的旋转的三个术语,它们分别对应于不同的旋转轴:

pitch

俯仰定义为给定对象绕 y 轴的旋转。
俯仰角:Pitch 是围绕y 轴的旋转,它决定了对象的上下倾斜程度。想象一下,当你点头或者抬头时,你的头部就是在做 Pitch 运动。
image

yaw

旋转定义为物体绕 z 轴的旋转
Yaw(偏航角):Yaw 是围绕 z 轴的旋转,它决定了对象的左右转向。比如,当你左右转头时,你的头部就是在做 Yaw 运动。
image

roll

Roll(翻滚角):Roll 是围绕 x 轴的旋转,它影响对象的倾斜。想象一下,当你倾斜头部,使耳朵接近肩膀时,这就是 Roll 运动。
image

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

4.虚幻引擎中的C++

设置vs界面

vs菜单栏-右键-自定义
命令-工具栏-标准
解决方案配置
解决方案平台
启动项目

修改所选内容
image

C++

image
image

image
image
image

反射与垃圾回收 Reflection and Garbage Collection

反射是程序在运行时检查自身的能力
程序将分析其自身内部发生的情况并收集有关该程序的数据。
C++没有内置反射,但虚幻引擎有自己设计的反射系统来收获这些数据
该反射系统负责将数据与Unreal Ed系统合并将其中一些数据暴露给蓝图并使用垃圾自动处理内存分配

蓝图子类的运行时机早于该蓝图的父类C++

【item蓝图继承自item C++类时】item蓝图先运行,然后item C++类运行

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

5. The Actor Class

1. 创建 Actor C++

将头文件放入public文件夹,cpp放入私有文件夹
image
image
image

新建文件后,等待 live coding 完成,再点击VS的 全部重新加载
item Actor类 的位置
image
这将显示该类的父类,所属模块,路径
image

双击改类将在代码编辑中打开文件

PCGdemo\Source\PCGdemo\Public\Items\Item.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once //防止重复包含该头文件

#include "CoreMinimal.h" //最小核心功能
#include "GameFramework/Actor.h" // AActor 来源
#include "Item.generated.h" //反射系统 必须为最后一行头文件

UCLASS()
class PCGDEMO_API AItem : public AActor
{
	GENERATED_BODY()//主体宏 编译时替换为实际代码 以增强C++
	
public:	
	// 设置此参与者属性的默认值
	AItem();//构造函数

protected:
	// 在游戏开始或生成时调用
	virtual void BeginPlay() override;//重载覆盖父类的虚函数

public:	
	// 每帧调用
	virtual void Tick(float DeltaTime) override;

};

PCGdemo\Source\PCGdemo\Private\Items\Item.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Items/Item.h"

// 设置默认值
AItem::AItem()
{
 	// 将此actor设置为每帧都调用 Tick()函数。如果不需要,可以关闭此功能以提高性能。
	// PrimaryActorTick 为父类的结构体类型 FActorTickFunction 用来包含一系列的变量 例如 bCanEverTick
	// 定义结构体 可防止该类被大量变量淹没
	PrimaryActorTick.bCanEverTick = true;

}

// 在游戏开始或生成时调用
void AItem::BeginPlay()
{
	// 使用范围解析运算符 调用父类 BeginPlay
	// 即调用该函数的 Actor 版本
	Super::BeginPlay();
}

// 每帧调用
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

将 Actor C++ 对象拖入世界

image
未编写功能的 原始Actor C++ 没有显示对象,拖入世界后将 不显示网格体之类的元素
通常不会直接放置原始 Actor C++
因此可以基于Actor C++ 新建蓝图Actor

基于 Actor C++ 创建 Actor 蓝图

Actor 蓝图则是 Actor C++ 的子类,继承 Actor C++ 的所有功能
蓝图提供用户友好的方式与类互动

在内容区 新建蓝图类 BP_Item

image

将 BP_Item 拖入关卡
image

双击 BP_Item 进入蓝图编辑器
image

Actor 有自己的本地坐标
Actor 必须有一个根组件,默认为 DefaultSceneRoot 场景根组件

顶部标签组
image

事件图是我们可以放置蓝图节点来执行蓝图逻辑的地方
同C++

Begin play 在游戏开始时执行

添加打印
需要 取消勾选 情景关联 以使用开发功能
image
image

更新蓝图后需要 编译并保存
image
image

播放时界面显示调试信息 hello
image

print string 更像是一个 C++ 函数,而 Begin play 是一个 C++ 事件

将鼠标悬停在开始播放事件上的这个小方块,它会显示输出委托,表示这是委托事件
image

构造脚本

image
它在游戏开始前被调用
只要该蓝图上的属性发生更改,就会执行它
例如,我们可以进入蓝图的详细信息面板并更改其中的变量属性,每次我们更改某些内容时,该构造脚本都会启动
所以这对于在游戏开始之前做事情很有好处

C++ Begin play / 日志宏

PCGdemo\Source\PCGdemo\Private\Items\Item.cpp

// 在游戏开始或生成时调用
void AItem::BeginPlay()
{
	// 使用范围解析运算符 调用父类 BeginPlay
	// 即调用该函数的 Actor 版本
	Super::BeginPlay();

	// 第一个参数:日志类别
	// 第二个参数:详细程度
	// 第三个参数:内容,或可变参数
	// TEXT() 为文本宏 接受字符串参数,可将字符串转换为 Unicode格式
	// Unicode 比 默认的 ANSI 具有更多的字符
	// 虚幻引擎要求始终对字符串使用文本宏 TEXT() 包裹
	// FString 为虚幻字符串类型
	UE_LOG(LogTemp,Warning,TEXT("hello world"));
}

保存代码,运行 live coding [失效时可关闭虚幻编辑器,编译VS代码]

image

image

使用C++ 在屏幕上显示调试消息

在开放世界地图中,如果摄像机远离指定Actor,即跨越相关的地图分区,该actor则与玩家不在相关,该actor执行的调试信息将不再显示。

新建普通地图进行测试

log string 控制台日志

image
image

按D键可复制蓝图

print string 屏幕日志

Key 允许我们指定一个值来确定这些打印字符串消息的行为
如果我们不设置它,较新的消息显示在顶部,并且将两条消息打印到屏幕上

如果这两个打印字符串都使用相同的key,则新消息将替换旧消息
而不是像现在这样堆积起来

如果我将第一个打印字符串的键设置为 1,并将第二个打印字符串的键也设置为 1,

然后当我点击播放时,我只会看到第二个,因为第一个被第二个取代

Delta秒是一个浮点值,为我们提供自上次以来已经过去的时间量

C++ GEngine->AddOnScreenDebugMessage

PCGdemo\Source\PCGdemo\Private\Items\Item.cpp

// 在游戏开始或生成时调用
void AItem::BeginPlay()
{
	// 使用范围解析运算符 调用父类 BeginPlay
	// 即调用该函数的 Actor 版本
	Super::BeginPlay();

	// Gengine 是一个UEngine  类型的指针变量:UEngine *
	// 全局引擎指针可以为零,所以不要在没有检查的情况下使用
	// 如果它的值为零,则意味着它是一个空指
	// 如果尝试引用空指针,程序可能会崩溃
	// 使用箭头运算符 调用指针的方法
	if (GEngine)
	{
		// 第一个参数为key,决定调试信息堆叠行为
		// 第二个为显示时间 秒
		// 第三个为显示颜色 FColor 具有静态变量 Cyan , 不需实例化类为对象,可以直接在类上访问类的静态变量
		// 第四个为FString 类型 消息
		GEngine->AddOnScreenDebugMessage(1, 60.F, FColor::Cyan, FString("item on screen"));
	}
}

image

打印增量时间

PCGdemo\Source\PCGdemo\Private\Items\Item.cpp
FString 可以保证字符串跨平台

// 每帧调用
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// 可变参数
	// %f 浮点格式
	// DeltaTime 将按顺序替换占位符 %f
	UE_LOG(LogTemp, Warning, TEXT("DelteTime: %f"), DeltaTime);

	if (GEngine)
	{
		//GetName 可获取当前游戏对象的名称
		FString Name = GetName();
		// Name 字符串必须使用星号前缀,FString 重载了星号运算符,这里不是指针,而是C风格的字符串,C风格的字符串是字符数组
		// 所以对FString::Printf的字符串参数不能是FString字符串对象,而必须是C风格的字符串
		FString Message = FString::Printf(TEXT("DelteTime: %f,----name: %s"), DeltaTime, *Name);
		GEngine->AddOnScreenDebugMessage(1, 60.f, FColor::Blue, Message);
	}
}

image

VS中选中 FString - 速览定义 可查看定义
image

绘制调试球体 Drawing Debug Spheres

蓝图中绘制

获取自身位置-绘制球体
image

image

C++ 中绘制

需要包含头文件 绘制调试助手
PCGdemo\Source\PCGdemo\Private\Items\Item.cpp

#include "DrawDebugHelpers.h"

E:\Unreal Projects 532\PCGdemo\Source\PCGdemo\Private\Items\Item.cpp

void AItem::BeginPlay()
{
	Super::BeginPlay();

	//BeginPlay 开始游戏是 世界对象通常为空,尚未准备
	//返回世界对象指针 可能为空
	UWorld* World = GetWorld();

	if (World)
	{
		// 第一个参数为 世界对象
		// 第二个为球体中心位置向量
		// 第三个为球体半径
		// 第四个球体段数 虚幻要求使用32位整数 通用,int32
		//第五个为 颜色
		//第六个 持续存在 bool 默认为true ,这里传false以实时更新状态和位置
		//第七 持续时间 
		//其余可选
		FVector Location = GetActorLocation();//返回当前actor根组件的世界位置
		DrawDebugSphere(World, Location, 25.f, 24, FColor::Red, false, 30.f);
	}

}

image

封装球体绘制宏

THIRTY 30 数字宏

#define THIRTY 30

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (GEngine)
	{
		FString Name = GetName();
		FString Message = FString::Printf(TEXT("DelteTime: %f,----name: %s"), DeltaTime, *Name);
		GEngine->AddOnScreenDebugMessage(1, THIRTY, FColor::Blue, Message);
	}
}

DRAW_SPHERE(Location)

#include "Items/Item.h"
#include "DrawDebugHelpers.h"

#define THIRTY 30
// 编译时 DRAW_SPHERE(Location) 将被后方的代码替换
#define DRAW_SPHERE(Location) if (GetWorld()) DrawDebugSphere(GetWorld(), Location, 25.f, 12, FColor::Red, true);

void AItem::BeginPlay()
{
	Super::BeginPlay();
	UWorld* World = GetWorld();

	if (World)
	{
		
		FVector Location = GetActorLocation();
		DRAW_SPHERE(Location);
	}

蓝图绘制调试线 Drawing Debug Lines

image

draw debug line 需要起点和终点坐标

起点用当前actor坐标表示

get actor forward vector 将获取对象的方向向量,但长度为1,
当前actor坐标 + 方向向量,即朝该方向移动1cm,终点坐标相对actor的指定方向偏移1cm,这样画出来的调试线过短。
故需要将get actor forward vector 乘以100,成为长度100的向量,这样 终点坐标相对actor的指定方向偏移100cm,显示较好

C++ 绘制调试线 Drawing Debug Lines

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Items/Item.h"
#include "DrawDebugHelpers.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();

	UWorld* World = GetWorld();
	FVector Location = GetActorLocation();
	if (World) {
		// 获取当前actor前向向量 长度为1
		FVector Forward = GetActorForwardVector();
		// true 表示不消失
		// -1.f 表示生命周期 由于已经不消失,故该参数无意义,使用-1
		// 0为深度优先级 即是否覆盖在其他线之上
		// 2.f 表示线的厚度
		DrawDebugLine(World, Location, Location + Forward * 100.f, FColor::Red, true, -1.f, 0, 2.f);
	}
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

image

线段宏

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Items/Item.h"
#include "DrawDebugHelpers.h"

#define DRAW_LINE(StartLocation, EndLocation) if (GetWorld()) DrawDebugLine(GetWorld(), StartLocation, EndLocation, FColor::Red, true, -1.f, 0, 1.f);


AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();

	UWorld* World = GetWorld();
	FVector Location = GetActorLocation();
	FVector Forward = GetActorForwardVector();
	DRAW_LINE(Location, Location + Forward * 100.f);
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

蓝图绘图调试点 Drawing Debug Points

调试点大小相对视图保持不变,无论距离相机多远,始终可以被看到。
image
image
image

C++ 绘图调试点 Drawing Debug Points

如果宏自带分号结尾,在调用宏的时候依然可以增加分号结尾。
宏内代码换行时,每行结尾添加反斜杠\,但最后一行代码不需要添加反斜杠

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Items/Item.h"
#include "DrawDebugHelpers.h"

#define DRAW_POINT(Location) if (GetWorld()) DrawDebugPoint(GetWorld(), Location, 15.f, FColor::Red, true);

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();

	UWorld* World = GetWorld();
	FVector Location = GetActorLocation();
	FVector Forward = GetActorForwardVector();
	DRAW_POINT(Location + Forward * 100.f);
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

封装宏,在VS项目中添加自定义头文件

在source内的项目名称根目录处右键-添加-新建项-显示所有模板-头文件-命名为 DebugMacros
image
image
更改文件目录为项目根目录 \Learn\Source\Learn ,与public同级
image
image
点击添加即可

E:\Unreal Projects 532\Learn\Source\Learn\DebugMacros.h

#pragma once
#include "DrawDebugHelpers.h"

#define DRAW_SPHERE(Location) if (GetWorld()) DrawDebugSphere(GetWorld(), Location, 25.f, 12, FColor::Red, true);
#define DRAW_SPHERE_COLOR(Location, Color) DrawDebugSphere(GetWorld(), Location, 8.f, 12, Color, false, 5.f);
#define DRAW_SPHERE_SingleFrame(Location) if (GetWorld()) DrawDebugSphere(GetWorld(), Location, 25.f, 12, FColor::Red, false, -1.f);
#define DRAW_LINE(StartLocation, EndLocation) if (GetWorld()) DrawDebugLine(GetWorld(), StartLocation, EndLocation, FColor::Red, true, -1.f, 0, 1.f);
#define DRAW_LINE_SingleFrame(StartLocation, EndLocation) if (GetWorld()) DrawDebugLine(GetWorld(), StartLocation, EndLocation, FColor::Red, false, -1.f, 0, 1.f);
#define DRAW_POINT(Location) if (GetWorld()) DrawDebugPoint(GetWorld(), Location, 15.f, FColor::Red, true);
#define DRAW_POINT_SingleFrame(Location) if (GetWorld()) DrawDebugPoint(GetWorld(), Location, 15.f, FColor::Red, false, -1.f);
#define DRAW_VECTOR(StartLocation, EndLocation) if (GetWorld()) \
	{ \
		DrawDebugLine(GetWorld(), StartLocation, EndLocation, FColor::Red, true, -1.f, 0, 1.f); \
		DrawDebugPoint(GetWorld(), EndLocation, 15.f, FColor::Red, true); \
	}
#define DRAW_VECTOR_SingleFrame(StartLocation, EndLocation) if (GetWorld()) \
	{ \
		DrawDebugLine(GetWorld(), StartLocation, EndLocation, FColor::Red, false, -1.f, 0, 1.f); \
		DrawDebugPoint(GetWorld(), EndLocation, 15.f, FColor::Red, false, -1.f); \
	}

VS编辑器识别缓慢,初次新建代码不识别虚幻库,需编译等待

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Items/Item.h"
// 编译器将从当前目录开始搜索,然后上一级,直到public
// Learn/DebugMacros.h 将从 Learn 开始搜索
#include "Learn/DebugMacros.h"

// 对于public目录下的文件,引入时写相对public目录的路径即可,例如 public/abc.h ,引入时为 #include "abc.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();

	UWorld* World = GetWorld();
	FVector Location = GetActorLocation();
	FVector Forward = GetActorForwardVector();
	DRAW_POINT(Location + Forward * 100.f);
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}


手动创建

在编辑器外手动创建文件时[不推荐,易报错],以及虚幻引擎外新建的C++文件[不推荐,易报错],需要重新生成VS项目,否则编辑器不同步显示文件,导致报错

右键 PCGdemo.uproject 运行 generate visual studio project files 重新生成VS项目
image
image

编辑器内删除的文件,并未在本地磁盘中实际删除,需要手动删除

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

6.使用C++代码移动对象 Moving Objects With Code

SetActorLocation 位置

蓝图中设置 SetActorLocation

image

C++ 中设置 SetActorLocation

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Items/Item.h"
#include "Learn/DebugMacros.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();

	UWorld* World = GetWorld();
	SetActorLocation(FVector(0.f, 0.f, 50.f));

	FVector Location = GetActorLocation();
	FVector Forward = GetActorForwardVector();

	DRAW_SPHERE(Location);
	DRAW_POINT(Location + Forward * 100.f);
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

SetActorRotation 旋转

蓝图

actor绕Z轴旋转90度
image

C++

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Items/Item.h"
#include "Learn/DebugMacros.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();

	UWorld* World = GetWorld();
	SetActorRotation(FRotator(0.f, 45.f, 0.f));//顺时针旋转45度

	FVector Location = GetActorLocation();
	FVector Forward = GetActorForwardVector();

	DRAW_SPHERE(Location);
	DRAW_LINE(Location, Location + Forward * 100.f);
	DRAW_POINT(Location + Forward * 100.f);
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

image

Actor World Offset 偏移

蓝图

蓝图子类的运行早于父类C++,所以父类设置位置会使蓝图偏移失效
image

C++

连续偏移,旋转
E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Items/Item.h"
#include "Learn/DebugMacros.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	//DeltaTime 为 每帧秒 / 每帧经过的秒数 根据硬件不同 例如 1/30 秒 ,1/60 秒等等,极小
	float MovementRate = 50.f; // cm/s 每秒50厘米
	float RotationRate = 45.f;

	// DeltaTime为距离上一帧的时间
	// MovementRate * DeltaTime = 运动速率 cm/s  * 时间增量 s/frame = cm/frame  //秒数s相互抵消 以固定移动速度 cm/frame 每帧的厘米数
	// 最终实现每秒50厘米的速度
	AddActorWorldOffset(FVector(MovementRate * DeltaTime, 0.f, 0.f));

	AddActorWorldRotation(FRotator(0.f, RotationRate * DeltaTime, 0.f));

	DRAW_SPHERE_SingleFrame(GetActorLocation());
	DRAW_VECTOR_SingleFrame(GetActorLocation(), GetActorLocation() + GetActorForwardVector() * 100.f);
}

三角函数 Trig Functions

正弦函数是一个在数学中非常重要的三角函数,通常用符号 sin 表示。它定义为直角三角形中对边与斜边的比值,也可以在单位圆上定义。正弦函数有两种常见的角度表示方法:度数(角度)和弧度。

  1. 角度(Degrees):是一种测量角大小的单位,其中一个完整圆被划分为 360 度。在三角函数中,当提到角度时,我们通常是指这个度量。

  2. 弧度(Radians):是一种更自然的方式来衡量角度,尤其在数学和物理学中。一个完整的圆等于 (2\pi) 弧度。弧度基于圆的半径,一个弧度等于半径长度的圆弧。

正弦函数的值随角度的变化而变化,无论是以度数还是弧度表示。在不同的应用中,可能会根据需要使用度数或弧度。例如,在物理学中,弧度是更常用的单位,而在一些工程和导航应用中,则可能更多地使用度数。

正弦函数在周期为 2π 弧度或 360 度的区间内变化,呈现周期性波动。

The Sine Function

正弦波动蓝图[移动]

蓝图 左侧可以添加变量
image

变量节点可以直接拖入编辑器的时间图表中
可以将 变量 T 拖出并释放,然后我们就可以获取它或设置它
按住 alt 键单击可以切断连线

将每帧的 T 值设置为其旧值加上增量时间
因此,每一帧我们都会将 Delta 时间添加到称为 T 的运行时间变量中
image

虚幻引擎有两种版本的正弦函数,一种采用弧度radians,另一种采用角度degrees
image

选择正弦弧度
为sin函数传递时间,正弦是一个返回值的函数,随着输入值的增加,它会平滑地上下波动

调用函数 ad Actor World 偏移量,改变每一帧的 Z 值,

image

右键-delta location-分割结构体引脚-以单独更改Z
image
image
正弦会增加和减少,增加和减少直到正数,一遍又一遍地回到负值,但方式很平稳。
image

控制波动幅度
image

正弦波动 C++[不移动]

初始化变量的方式:
1-

private:
	float RunningTime;
	float Amplitude = 0.25f; //振幅

2

AItem::AItem() :Amplitude(0.25f)
{
	PrimaryActorTick.bCanEverTick = true;
}

3-技术是 赋值,非初始化,对比1和2,3(赋值)效率最低。

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;

	Amplitude = 0.25f;
}

上下波动
E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Item.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"

UCLASS()
class LEARN_API AItem : public AActor
{
	GENERATED_BODY()

public:
	AItem();

protected:

	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

private:
	float RunningTime;
	float Amplitude = 0.25f; //振幅
	float TimeConstant = 5.f;//正弦上升所需时间
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Items/Item.h"
#include "Learn/DebugMacros.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	RunningTime += DeltaTime;

	// 正弦函数是一个静态函数
	//传递的运行时间
	//Delta Z 将是这个正弦函数的结果
	//上下移动的距离称为正弦波的振幅
	//用正弦运动产生波,并且正弦波有振幅
	//将其乘以 Amplitude 0.25 F,那么我们将振幅设置为 0.25
	//将运行时间乘以一个值 TimeConstant 来加速正弦波
	float DeltaZ = Amplitude * FMath::Sin(RunningTime * TimeConstant);

	//将此更改添加到actor位置,以便我们可以调用actor世界偏移
	AddActorWorldOffset(FVector(0.f, 0.f, DeltaZ));

	DRAW_SPHERE_SingleFrame(GetActorLocation());
	DRAW_VECTOR_SingleFrame(GetActorLocation(), GetActorLocation() + GetActorForwardVector() * 100.f);
}

将C++变量暴露在蓝图中 Exposing Variables to Blueprint

这对于在游戏开始之前更改这些变量的值非常有用

蓝图的细节面板继承了许多来自C++的变量
image
可以自定义C++的变量公开给蓝图,
能够选择我们在世界上的一个actor并更改单个实例的属性而不影响蓝图本身

UPROPERTY(EditDefaultsOnly)

	// UPROPERTY() 将属性暴露给虚幻反射系统
	// EditDefaultsOnly 只公开给蓝图,蓝图细节面板可编辑该属性
	// 实例的细节面板不显示,也不可编辑该属性
	UPROPERTY(EditDefaultsOnly)
	float Amplitude = 0.25f; //振幅

UPROPERTY(EditInstanceOnly)

	//  只公开给实例,实例细节面板可编辑该属性
	// 蓝图的细节面板不显示,也不可编辑该属性
	UPROPERTY(EditInstanceOnly)
	float TimeConstant = 5.f;//正弦上升所需时间

UPROPERTY(EditAnywhere)

	//  公开给蓝图和实例,蓝图和实例细节面板都可编辑该属性
	UPROPERTY(EditAnywhere)
	float Amplitude = 0.25f; //振幅

实例的值会覆盖蓝图的值

可见但不可编辑 Visible But Not Editable

UPROPERTY(VisibleDefaultsOnly)

	//  蓝图中可见,但不可编辑
	//  实例中不可见
	UPROPERTY(VisibleDefaultsOnly)
	float RunningTime;

UPROPERTY(VisibleInstanceOnly)

	// 实例中可见,但不可编辑
	//  蓝图中不可见
	UPROPERTY(VisibleInstanceOnly)
	float RunningTime;

UPROPERTY(VisibleAnywhere)

	// 实例和蓝图中都可见,但不可编辑
	UPROPERTY(VisibleAnywhere)
	float RunningTime;

image

将变量暴露在事件图中 Exposing Variables to the Event Graph

这对于在游戏运行时更改这些变量的值非常有用【事件中更改】

BlueprintReadOnly

public:
	// EditAnywhere 公开给蓝图和实例,蓝图和实例细节面板都可编辑该属性
	// BlueprintReadOnly 必须为 public ,protected 属性
	// BlueprintReadOnly 在蓝图事件中只读,不可修改
	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	float Amplitude = 0.25f; //振幅

BlueprintReadWrite

只能是公开或保护类型

	// EditAnywhere 公开给蓝图和实例,蓝图和实例细节面板都可编辑该属性
	// BlueprintReadOnly 必须为 public ,protected  属性
	// BlueprintReadOnly 在蓝图事件中可读可写
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float Amplitude = 0.25f; //振幅

蓝图左侧显示从父类继承的变量

image
必须是公开类型 public 或保护类型 protected ,且 BlueprintReadWrite ,BlueprintReadOnly 才可以被继承

阻尼振荡

clamp 将振幅骑限制在 0-1 之间
振幅会越来越小到静止
image

属性分组

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sine Parameters")
	float Amplitude = 0.25f; //振幅

元数据 让蓝图事件图访问私有变量

事件图可访问,但只读

private:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	float TimeConstant = 5.f;//正弦上升所需时间

向蓝图公开方法 Exposing Functions to Blueprint

UFUNCTION(BlueprintCallable)

protected:
	// 变幻的正弦函数
	// BlueprintCallable 蓝图的事件图可调用
	UFUNCTION(BlueprintCallable)
	float TransformedSin(float Value);
float AItem::TransformedSin(float Value)
{
	return Amplitude * FMath::Sin(Value * TimeConstant);
}

上下左右波动

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"

UCLASS()
class LEARN_API AItem : public AActor
{
	GENERATED_BODY()

public:
	AItem();

protected:

	virtual void BeginPlay() override;

protected:
	// 变幻的正弦函数
	// BlueprintCallable 蓝图的事件图可调用
	UFUNCTION(BlueprintCallable)
	float TransformedSin(float Value);

	// 变幻的余弦函数
	// BlueprintCallable 蓝图的事件图可调用
	UFUNCTION(BlueprintCallable)
	float TransformedCos(float Value);
public:
	virtual void Tick(float DeltaTime) override;

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sine Parameters")
	float Amplitude = 0.25f; //振幅

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Sine Parameters")
	float RunningTime;

private:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	float TimeConstant = 5.f;//正弦上升所需时间
};
#include "Items/Item.h"
#include "Learn/DebugMacros.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();
}

float AItem::TransformedSin(float Value)
{
	return Amplitude * FMath::Sin(Value * TimeConstant);
}

float AItem::TransformedCos(float Value)
{
	return Amplitude * FMath::Cos(Value * TimeConstant);
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	RunningTime += DeltaTime;
	//float DeltaZ = Amplitude * FMath::Sin(RunningTime * TimeConstant);
	//AddActorWorldOffset(FVector(0.f, 0.f, DeltaZ));

	DRAW_SPHERE_SingleFrame(GetActorLocation());
	DRAW_VECTOR_SingleFrame(GetActorLocation(), GetActorLocation() + GetActorForwardVector() * 100.f);
}

公开的方法可以在蓝图事件表中 右键搜索到

纯蓝图函数 上下左右波动

纯蓝图函数不会改变actor的属性,例如不能在纯蓝图函数内部调用世界偏移方法[AddActorWorldOffset]

protected:
	// 蓝图的事件图可调用
	UFUNCTION(BlueprintPure)
	float TransformedSin();

	// 蓝图的事件图可调用
	UFUNCTION(BlueprintPure)
	float TransformedCos();
#include "Items/Item.h"
#include "Learn/DebugMacros.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AItem::BeginPlay()
{
	Super::BeginPlay();
}

float AItem::TransformedSin()
{
	return Amplitude * FMath::Sin(RunningTime * TimeConstant);
}

float AItem::TransformedCos()
{
	return Amplitude * FMath::Cos(RunningTime * TimeConstant);
}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	RunningTime += DeltaTime;
	//float DeltaZ = Amplitude * FMath::Sin(RunningTime * TimeConstant);
	//AddActorWorldOffset(FVector(0.f, 0.f, DeltaZ));

	DRAW_SPHERE_SingleFrame(GetActorLocation());
	DRAW_VECTOR_SingleFrame(GetActorLocation(), GetActorLocation() + GetActorForwardVector() * 100.f);
}

image
image

模板函数 Template Functions [类型作为参数]

模板函数的实现必须写在类型定义文件中
E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Item.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"

UCLASS()
class LEARN_API AItem : public AActor
{
	GENERATED_BODY()

public:
	template<typename T>
	T Avg(T First, T Second);
};

template<typename T>
inline T AItem::Avg(T First, T Second)
{
	// return T();
	return (First + Second) / 2;
}

调用模板函数

void AItem::BeginPlay()
{
	Super::BeginPlay();
	int32 AvgInt = Avg<int32>(1, 3);

	float AvgFloat = Avg<float>(3.45f, 7.8f);

	// 取两个向量的平均值,两点中间位置
	FVector AvgVector = Avg<FVector>(GetActorLocation(), FVector::ZeroVector);
}

组件 Components

actor可以拥有组件,这是为actor提供功能的一种方式。
image

假设我们有一个名为 Weapon 的Actor。
需要一个网格,因此我们将为此提供一个组件。
就可以看到我们的武器在世界上的样子。

当您挥动武器时,您可能希望知道武器何时击中物体。
可以在武器网格的刀片周围放置一个不可见的碰撞盒子组件,当我们击中敌人时可以用它来检测重叠事件。

每个Actor至少有一个组件,一旦我们创建了蓝图项目,我们就在组件中看到它有一个默认的场景根组件。

但在C++中,它被称为RootComponent,其类型是 USceneComponent。
场景组件的功能非常有限,具有 Transform 功能。
包括 Location,Rotation,Scale。
场景组件能够附加到其他组件。

每当我们调用 get actor location 并获取代表Actor位置的 f 向量时。
这个函数实际上返回根组件的位置。
换句话说,它返回存储在根组件中的变换变量Transform。
这就是为什么一个 actor 必须至少有一个组件,即包含该变换的根组件。

由于场景组件支持附件,我们可以向Actor添加一个新的场景组件将其附加到根。
通过附加到根,这意味着场景组件将随着根组件,始终保持它们的相对距离恒定。
因此,如果根组件在世界中移动,场景组件也会随之移动。

场景组件派生的其他一些类:
静态网格体组件
image
组件面板中的静态网格物体名称并不意味着我们刚刚添加了一个新的静态网格物体。
相反,我们添加了一个新的静态网格体组件对象。

静态网格体组件有自己的变换,并且可以附加到其他组件。
静态网格物体组件类有一个Static Mesh 变量。
静态网格物体组件有一个静态网格物体变量,所以我们需要指定使用什么网格物体
image

静态网格物体的枢轴点位于网格的底部,而不是网格的中心
image
image
所以在建模软件中,枢轴点也要在底部

按 E 进入旋转模式并旋转它

静态网格体组件源自场景组件。
所以它们实际上本身就是场景组件,这意味着我们可以重新分配静态网格物体组件成为默认场景根。
只需单击静态网格并将其拖动到默认场景根,默认根目录将被删除
image
现在我们的根是静态网格物体组件,这将无法在视口中移动该网格物体,因为这现在是蓝图的参考框架。
它永远不会相对于自身移动

Components in C++

类默认对象 CDO

当我们在虚幻引擎中创建一个类时,比如我们的Item类,它派生于actor类,
虚幻引擎会根据该类创建一个类默认对象或 CDO。
类默认对象是虚幻引擎反射系统中包含的对象的主副本。
image
它是在引擎启动时创建的。
编译我们的代码本质上也开始了它的生成。
它保存反射系统可以访问的默认值来初始化属性。

基于该类,在世界中创建的蓝图对象。
当引擎初始化时,它为每个类创建这些类默认对象。
然后它执行每个类的构造函数,设置它们的默认值。
蓝图获取由引擎初始化的默认值,并且该信息来自类和默认对象。
所有这一切都发生在我们的幕后,所以我们可以制作游戏,甚至不知道这个过程正在发生。
image

了解这个过程很重要,因为给定类的构造函数在游戏引擎进程的早期执行,通常在游戏构造函数中做某些事情为时过早。
如果我们想在游戏开始时做某事并确保游戏中的所有对象都已初始化,我们必须在begin play 开始播放时而不是在对象的构造函数中进行初始化

子对象

假设我们有Item类,并且添加了一个名为Item mesh的组件。
要添加这个组件,我们必须为此组件创建一个默认的子对象。
默认子对象的行为很像类默认对象,只是它用于组件本身.
它包含该子对象的所有默认属性.
虚幻引擎组件是其所属 actor 的子对象.

创建默认子对象时,必须提供一些信息
需要指定子对象的对象类型.
例如,使用静态网格组件,我们还需要提供一个内部名称.
该名称与实际变量名称本身不同,由虚幻引擎使用.
出于各种目的,主要是跟踪不同的对象以创建默认的子对象。

子对象可以采用不同的组件类型.
因此,在尖括号中我们提供了组件类型,例如使用静态网格组件,并且在括号中我们提供对象的名称,这就是内部名称.
将字符串文字包装在文本宏中.确保它们采用 Unicode 格式.
这就是我们为新组件创建默认子对象的方式
image

创建默认子对象返回新创建的子对象的地址,以便我们可以存储它到一个指针中

为我们创建对象的函数称为工厂函数。
在 C++ 中,您可能习惯使用 new 关键字来构造对象的新实例,并且将该对象的地址存储在指针中。
在虚幻引擎中,我们很少使用 new 关键字,因为引擎具有这些工厂函数为我们构造对象并执行大量内部工作,例如注册该对象用以让引擎知道它

要在 C++ 中创建一个新组件,我们必须创建一个默认的子对象。
image

Item网格指针

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Item.h

private:
	// 静态网格体组件类型的指针
	// 只是一个空指针
	// UPROPERTY() 反射负责垃圾回收 指针为空时删除对象
	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent* ItemMesh;

有了Item网格指针,需要实际构造一个新的静态网格组件

在构造函数中,将创建一个新的静态网格物体组件

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;

	// 在构造函数中,将创建一个新的静态网格物体组件
	// 为此,必须创建默认的子对象
	// 这是一个模板函数 需要在此处提供子对象的类型
	// 在括号中,使用文本宏添加该组件的内部名称
	// 这会创建默认的子对象
	// 这个工厂函数返回一个指向新创建的对象的指针
	// 将其存储在Item网格指针变量中
	ItemMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ItemMeshComponent"));

	// 根组件可以重新分配给不同的场景组件
	// 静态网格物体组件,它派生自场景组件
	// 将根组件变量重新分配给Item网格
	// 就像在蓝图中一样,根组件指针变量存储的默认场景根的地址将被自动删除,那是因为垃圾收集系统将发现根组件不再指向它。
	// 现在根组件指针指向新创建的Item网格子对象
	RootComponent = ItemMesh;
}

继承该类的BP_Item蓝图效果如下
image
image

因为静态网格体组件的静态网格变量尚未设置,所以当前视口中看不到任何东西。
最好的做法是在 C++ 中创建静态网格体组件并在蓝图设置其静态网格体属性,使得它更加通用。
因为我们可以创建多个Item蓝图并单独设置它们的静态网格属性。
image

在编辑器中,虚幻引擎为拆分C++定义的名称使其更加可读。
蓝图中的 Item Mesh 对应 C++ 中的 ItemMesh 变量
image

蓝图中 ItemMeshComponent 对应 C++ 中CreateDefaultSubobject 文本宏包裹的 ItemMeshComponent
image
通常不会让场景组件在任何地方进行编辑,而是让它们在任何地方都可见。
场景组件本身在详细信息面板中有自己的属性,可以更改这些属性。
包含位置、旋转和缩放的完整变换。
如果这个特定组件是根组件,我们就无法更改它的位置或其轮换

为其指定静态网格体
image
image

要更改其位置,我们只能在世界中actor的实例上执行此操作.
image

当前 BP_Item 在x,y轴做正弦晃动
image

更改蓝图使其在z轴上下正弦晃动
image

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

7. The Pawn Class

继承自 actor 类,但可以被控制器所拥有。

制作一只我们可以拥有并飞来飞去的鸟。

工具-新建C++类-选择 Pawn 类作为父类,创建新的C++类。
image
image
E:\Unreal Projects 532\Learn\Source\Learn\Public\Pawns\Bird.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Bird.generated.h"

UCLASS()
class LEARN_API ABird : public APawn
{
	GENERATED_BODY()

public:
	ABird();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

	// 调用以将功能绑定到输入
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Pawns\Bird.cpp

#include "Pawns/Bird.h"

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

}

void ABird::BeginPlay()
{
	Super::BeginPlay();
	
}

void ABird::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
}

基于 Bird 类 创建一个新的蓝图 BP_Bird

image

image
image

从市场下载动物包,导入鸟类

虚幻引擎-虚幻商城-ANIMAL VARIETY PACK

image

该资源-Rigged: Yes ,这意味着它们可以被动画化
点击-免费
点击-添加到工程
可以将 AnimalVarietyPack 目录复制到工程 Content 目录下

胶囊体组件 Capsule Component

由于 鸟的静态网格体三角形太多,检测碰撞需要大量计算,非常昂贵,

image

所以使用胶囊体组件 Capsule Component 进行碰撞检测。一个具有一些基本几何形状的组件。
image

image
它的几何形状非常简单,并且还能够在编辑器中渲染

C++

E:\Unreal Projects 532\Learn\Source\Learn\Public\Pawns\Bird.h 头文件中,#include "Bird.generated.h"必须放在所有头文件的底部,generated.h 将自动生成大量代码,与反射系统一起工作。

E:\Unreal Projects 532\Learn\Source\Learn\Public\Pawns\Bird.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"

// 胶囊体组件 Capsule Component 的依赖项
#include "Components/CapsuleComponent.h"
#include "Bird.generated.h"

UCLASS()
class LEARN_API ABird : public APawn
{
	GENERATED_BODY()

public:
	ABird();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

	// 调用以将功能绑定到输入
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

private:
	// 胶囊体组件指针变量,此时尚未创建胶囊对象
	UPROPERTY(VisibleAnywhere)
	UCapsuleComponent* Capsule;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Pawns\Bird.cpp

#include "Pawns/Bird.h"

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	// 为胶囊创建一个默认子对象,并在构造函数中执行此操作
	// 使用创建默认子对象工厂来为我们的胶囊创建默认子对象

	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));

	// 可以将胶囊作为我们的根组件
	// RootComponent = Capsule;

	// SetRootComponent(Capsule) 等同于 RootComponent = Capsule;
	SetRootComponent(Capsule);
}

void ABird::BeginPlay()
{
	Super::BeginPlay();

}

void ABird::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
}

打开 BP_Bird 蓝图[纯数据蓝图]-打开完整蓝图编辑器
image

在组件面板中,可以看到胶囊体组件
image
可以在那里放置一个人物角色

细节面板 修改胶囊体大小:

image

C++ 中设置胶囊提大小

#include "Pawns/Bird.h"

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	// 为胶囊创建一个默认子对象,并在构造函数中执行此操作
	// 使用创建默认子对象工厂来为我们的胶囊创建默认子对象

	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));

	// 设置半高
	// 参数1 半高
	// 参数2 重叠是否更新 默认true,胶囊被设置为响应重叠而触发事件
	// 可被蓝图同属性设置覆盖
	Capsule->SetCapsuleHalfHeight(20.f);

	// 设置半径
	// 参数1 半径
	// 参数2 重叠是否更新 默认true,胶囊被设置为响应重叠而触发事件
	// 可被蓝图同属性设置覆盖
	Capsule->SetCapsuleRadius(15.f);

	// 可以将胶囊作为我们的根组件
	// RootComponent = Capsule;

	// SetRootComponent(Capsule) 等同于 RootComponent = Capsule;
	SetRootComponent(Capsule);
}

void ABird::BeginPlay()
{
	Super::BeginPlay();

}

void ABird::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
}

现在可以使用胶囊进行碰撞检测出,而不必使用我们实际的鸟网格体。

前置声明 Forward Declaration

直接将 #include "Components/CapsuleComponent.h" 包含在 Bird.h 头文件中不是最佳实践。

image

前置声明代替直接包含

使用前置声明代替直接包含 b.h 有几个潜在的好处:

  1. 减少编译依赖:如果 a.h 只是使用了 b.h 中声明的一部分内容(如某些类或函数的引用),前置声明可以减少文件之间的编译依赖。这样做有助于加快编译速度,因为更改 b.h 不会直接导致需要重新编译包含 a.h 的所有文件。

  2. 避免循环依赖:在某些情况下,使用前置声明可以帮助避免头文件之间的循环依赖问题。

  3. 减少编译时间:虽然包含头文件通常不会显著增加单个文件的大小,但它可能增加编译时间,尤其是在大型项目中,头文件被多次包含时。

  4. 避免隐式包含:a.h 包含 b.h:【如果c包含了a】这意味着每个包含了 a.h 的文件也隐式地包含了 b.h。这可能导致 b.h 中定义的所有内容(如类、函数、变量等)在所有包含 a.h 的文件中都可用,从而增加了编译依赖和潜在的编译时间。

a.cpp 包含 b.h:这种情况下,b.h 中的内容只在 a.cpp 文件中可用。这意味着对 b.h 的依赖被局限在 a.cpp 中,减少了头文件间的编译依赖,从而可能减少编译时间和减小编译复杂性。

总之,如果您只需要引用类或函数的声明,而不需要访问其完整定义,那么使用前置声明是一个好的做法。这样可以减少不必要的编译依赖,提高编译效率。

预处理器编译 Bird.h文件

C++ 预处理器编译 Bird.h文件时,会将 #include "Components/CapsuleComponent.h" 字符串 替换为 CapsuleComponent.h头文件内的内容。
而 CapsuleComponent 包含了胶囊体的所有代码,包括当前程序不会使用的代码。
CapsuleComponent 文件还会包含其他文件,
image
预处理器会找到所有这些层层包含的文件代码,粘贴到 Bird.h文件中,
而实际编译器不需要知道所有细节,只需要知道用到的 UCapsuleComponent 的信息。
在 头文件中,只是创建了 UCapsuleComponent 类型的一个指针。
在 cpp中,才会实际创建 UCapsuleComponent 对象,此时,需要直到 UCapsuleComponent 的详细信息,在内存分配多少空间。
但是需要先在头文件定义 UCapsuleComponent 组件才能在 cpp中构造该类型组件,所以,可以在头文件中声明一个 UCapsuleComponent 名称的变量

class 前置声明

class 前置声明 之后的同类型不需要重复声明,但之前的同类型不可用,除非也使用class 前置声明
E:\Unreal Projects 532\Learn\Source\Learn\Public\Pawns\Bird.h

private:
	// 报错
	UCapsuleComponent* Capsule1;


	// 胶囊体组件指针变量,此时尚未创建胶囊对象
	// class 声明了 UCapsuleComponent 是一个类名,可用作类型
	UPROPERTY(VisibleAnywhere)
	class UCapsuleComponent* Capsule;

	// 可用
	UCapsuleComponent* Capsule2;

改头文件不需要再包含 #include "Components/CapsuleComponent.h"

在头文件中的 class UCapsuleComponent* Capsule; 这种语法是一个前置声明(Forward Declaration)与指针的结合使用。

这里的解释如下:

  • class UCapsuleComponent: 这是一个前置声明。它告诉编译器 UCapsuleComponent 是一个类名,即便在这一点上编译器还不知道这个类的具体定义。这种做法在头文件中很常见,特别是在处理交叉引用或减少编译依赖时。

  • * Capsule: 这表示 Capsule 是一个指向 UCapsuleComponent 类型的指针。由于这里只是声明了一个指针,所以不需要知道 UCapsuleComponent 类的完整定义。

综合起来,class UCapsuleComponent* Capsule; 这条语句在头文件中声明了一个指向 UCapsuleComponent 类型的指针,而无需包含整个 UCapsuleComponent 类的定义。这种做法在 C++ 中是用来减少头文件的相互依赖和编译时间的常用技巧。

这种类型是不完整类型,没有确定内存空间大小。
此时只能穿件该类型的指针,但不能访问该类的成员变量,和函数操作,以及创建该类型的实际对象。

在 cpp 将创建该类的实际对象,需要知道详细定义,可以包含该类的头文件
E:\Unreal Projects 532\Learn\Source\Learn\Private\Pawns\Bird.cpp

// 胶囊体组件 Capsule Component 的依赖项
#include "Components/CapsuleComponent.h"

在Bird.cpp中依然包含了头文件,但会将 CapsuleComponent 头文件限制在 该Bird.cpp中,不会被其他包含 Bird.h 的文件隐式包含,也不会造成循环依赖。

何时需要在头文件中包含其他头文件

从父类继承时,我们必须包含头文件

class LEARN_API ABird : public APawn 继承 APawn时,当前头文件已经开始访问APawn的属性和方法,所以需要包含 GameFramework/Pawn.h

需要类型大小时

使用工厂来构造类的实例需要包含该头文件
计划访问成员变量和函数

前置声明 建议放在文件靠前位置

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Bird.generated.h"

// 前置声明
class UCapsuleComponent;

UCLASS()
class LEARN_API ABird : public APawn
{
	GENERATED_BODY()

public:
	ABird();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

	// 调用以将功能绑定到输入
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

private:
	UPROPERTY(VisibleAnywhere)
	UCapsuleComponent* Capsule;
};

骨骼网格体组件 Skeletal Mesh Components

静态网格物体无法进行动画处理,
骨骼网格体组件可以制作动画。
image

就像静态网格物体组件有自己的静态网格物体变量一样.
骨骼网格体组件类有它自己的变量,称为骨骼网格物体 USkeletalMesh.
并且骨架网格物体有骨架 Skeleton.因为它有骨架,所以可以制作动画.

image

鸟的 骨骼网格体 资产

SK_Crow
image
image

骨骼网格体 资产面板包含了 材质,骨骼。
骨骼网格体 骨骼树 具由骨骼层次结构

显示骨骼:
角色-骨骼-所有层级
image
image
骨架中的骨骼已按权重绘制到网格上,当这些骨骼之一移动时,网格物体将随之扭曲并移动。
可以单击其中一些骨骼并移动它们。。
因为网格是权重绘制的,因此网格多边形上的顶点会跟随骨头。
image

绑定

将骨架连接到网格体并将网格体权重绘制到骨骼上称为绑定。
rig为网格体创建骨架,并使这些网格体能够与骨骼一起变形和动画。

动画

动画是包含与骨骼运动相关的数据的资产.网格可以设置动画.
image
包含这些骨骼的运动信息
image
该动画仅与该特定网格相关联.
右上可以切换 :骨骼网格体预览,骨骼网格体编辑器,骨骼动画。

骨骼网格体预览 可以查看共享骨骼的其他 骨骼网格体。
image

为鸟添加 骨骼网格体组件

USkeletalMeshComponent

E:\Unreal Projects 532\Learn\Source\Learn\Public\Pawns\Bird.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Bird.generated.h"

class UCapsuleComponent;

// 骨骼网格体声明
class USkeletalMeshComponent;

UCLASS()
class LEARN_API ABird : public APawn
{
	GENERATED_BODY()

public:
	ABird();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

	// 调用以将功能绑定到输入
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

private:
	UPROPERTY(VisibleAnywhere)
	UCapsuleComponent* Capsule;

	// 骨骼网格体组件 
	UPROPERTY(VisibleAnywhere)
	USkeletalMeshComponent* BirdMesh;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Pawns\Bird.cpp

#include "Pawns/Bird.h"
// 胶囊体组件 Capsule Component 的依赖项
#include "Components/CapsuleComponent.h"


ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetCapsuleHalfHeight(20.f);
	Capsule->SetCapsuleRadius(15.f);
	SetRootComponent(Capsule);

	// 骨骼网格体组件 子对象
	BirdMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("BirdMesh"));

	//  骨骼网格体组件派生自场景组件
	// 将 骨骼网格体组件 附加到根组件,以跟随根组件移动,即 Capsule 胶囊组件
	// 参数1 根组件
	// 参数2 插槽名称
	BirdMesh->SetupAttachment(GetRootComponent());
	//BirdMesh->SetupAttachment(Capsule);
	//BirdMesh->SetupAttachment(RootComponent);
}

void ABird::BeginPlay()
{
	Super::BeginPlay();

}

void ABird::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
}

打开 BP_Bird 蓝图
image
Capsule 胶囊组件 下有了 BirdMesh 骨骼网格体组件,
骨骼网格体组件-细节面板 -可以设置 骨骼网格体资产 和 动画。
image

设置一个 骨骼网格体资产 SK_Crow
image
image

现在它默认面向 Y 方向,
虚幻引擎actor应当面向x方向,红色箭头。
左下角为参照系
image

旋转 BirdMesh 骨骼网格体组件90度使其朝前。
image

向下移动 BirdMesh 骨骼网格体组件 使其底部与根组件胶囊体底部对齐
image

将 BP_Bird 拖入关卡创建实例
image
默认无法控制鸟的移动

为 BirdMesh 骨骼网格体组件 设置动画

BirdMesh 骨骼网格体组件-细节面板-动画-动画模式-使用动画资产

选择动画序列[底部绿色线段]
BirdMesh 骨骼网格体组件-细节面板-动画-要播放的动画-选择一个动画序列
image

此时视口将播放动画序列

绑定输入 Binding Inputs

在引擎中,只要您开始玩游戏,就会生成一个控制器并将其分配给玩家。
这个控制器是你在游戏中的代表。
游戏看到控制器并认为那是您,
控制器允许我们选择要占有的 Pawn 例如 BP_Bird
image

所有的 pawn 都有一个名为“自动拥有玩家 auto possess player”的变量,我们可以设置它的值.
它是未设置的,但如果我们将其设置为玩家0,则玩家0指的是世界上的第一个控制器.
控制器.从零开始计数.

如果世界上有多个控制器,例如在多人分屏游戏中,那么第一个控制器将是玩家零号,第二个控制器将是玩家一号.
在我们的例子中,只有一个控制器,这将是零号玩家.

通过设置我们的 pawn的 auto possess player变量为玩家零,只有我们游戏中的控制器才会拥有小鸟,然后我们就可以控制小鸟并让它飞.

虚幻引擎会在游戏开始时默认生成一个pawn供玩家控制【如果没有指定pawn】
点击该按钮即可和玩家控制器分离,查看引擎生成的默认pawn.[因为当前没有配置pawn]
image
image

为 BP_Bird 实例设置自动控制玩家

默认为禁用
image

该控制器指代当前电脑上的控制器,非多人网络游戏控制器。
当前电脑上的控制器应选玩家0,目前只有一个玩家。
image
此时按下播放将无法移动视角。因为控制器已经占有了BP_Bird实例对象,鸟。
按 shift+F1 即可看到,此时没有默认生成的灰色圆球。

从 C++ 设置 自动控制玩家 AutoPossessPlayer

E:\Unreal Projects 532\Learn\Source\Learn\Private\Pawns\Bird.cpp

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetCapsuleHalfHeight(20.f);
	Capsule->SetCapsuleRadius(15.f);
	SetRootComponent(Capsule);

	BirdMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("BirdMesh"));
	BirdMesh->SetupAttachment(GetRootComponent());


	// 设置 `自动控制玩家`的值为0号玩家 值为枚举EAutoReceiveInput
	AutoPossessPlayer = EAutoReceiveInput::Player0;
}

轴映射

编辑-项目设置-引擎-输入-操作映射,轴映射
image

设置轴映射 wsad
scale 为负值时表示反向

前/后 MoveForward

image

image

准备轴映射绑定函数
E:\Unreal Projects 532\Learn\Source\Learn\Public\Pawns\Bird.h

protected:
	// 绑定到轴映射的函数 向前/向后
	// 每帧调用该函数,接受参数:按下为1,无操作为0
	// 如果设置了轴映射缩放 则接受的参数为 scale *1 或 scale * 0
	// scale 为负值时表示反向
	void MoveForward(float Value);

E:\Unreal Projects 532\Learn\Source\Learn\Private\Pawns\Bird.cpp

void ABird::MoveForward(float Value)
{

}

// 将函数绑定到轴映射
void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	// 将前/后 回调函数绑定到轴映射
	// 参数1  MoveForward 对应项目输入轴映射设置
	// 参数2 用户对象指针 当前为鸟 pawn 指针
	// 参数3 回调函数的地址,使用 & 获取函数地址,但名称必须加上类名限定
	// &ABird::MoveForward 就是将一个函数的地址作为输出参数传递给另一个函数
	// PlayerInputComponent->BindAxis(TEXT("MoveForward"));
	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ABird::MoveForward);
}

添加运动输入 Adding Movement Input.

void ABird::MoveForward(float Value)
{
	// 每帧调用该函数,接受参数:按下为1,无操作为0
	// 如果设置了轴映射缩放 则接受的参数为 scale *1 或 scale * 0
	// scale 为负值时表示反向
	if (Controller && (Value != 0.f))
	{
		FVector Forward = GetActorForwardVector();//朝向
		// AddMovementInput 将方向和距离传递给 运动组件 例如 FloatingPawnMovement
		AddMovementInput(Forward, Value);//移动
	}
}

此时 BP_Bird 蓝图中默认没有 运动组件。

添加 运动组件 FloatingPawnMovement 【浮动pawn移动】

image

FloatingPawnMovement 细节面板可设置速度等属性
image

此时播放按W可以移动。

添加向后绑定S
image

完成前后绑定。

摄像头和弹簧臂 Camera and Spring Arm

使用蓝图为 BP_Bird 添加相机组件

选中胶囊组件,将相机添加到胶囊组件的子级,和胶囊组件一起移动。
image
将相机组件向后移动
image
可在关卡看到鸟
image

向上移动相机,然后向下倾斜相机,形成自上而下的视图。
image
image
这不是最佳实践,不应将相机直接连接到根组件,而应将相机连接到弹簧臂组件

添加 弹簧臂组件 Spring Arm 到根组件子级

将相机组件移到弹簧臂组件子级
image
此时相机的变换将错误,设置相机组件-细节面板-变换-位置 旋转 全部归零
image
弹簧臂组件 的变换也需要归零
红线表示弹簧臂组件
image
弹簧臂组件会根据相机与物体得到重叠自动收缩,可设置臂长
image
旋转 弹簧臂
image

使用C++ 为 Bird 添加相机组件 弹簧臂组件

class USpringArmComponent;
class UCameraComponent;

private:
	// 弹簧臂组件
	// 如果细节面板缺失,更换 SpringArm 名称为其他名称
	UPROPERTY(VisibleAnywhere)
	USpringArmComponent* SpringArm;

	// 相机组件
	UPROPERTY(VisibleAnywhere)
	UCameraComponent* ViewCamera;
// 弹簧臂
#include "GameFramework/SpringArmComponent.h"
// 相机
#include "Camera/CameraComponent.h"

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetCapsuleHalfHeight(20.f);
	Capsule->SetCapsuleRadius(15.f);
	SetRootComponent(Capsule);

	BirdMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("BirdMesh"));
	BirdMesh->SetupAttachment(GetRootComponent());

	//弹簧臂
	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArm->SetupAttachment(GetRootComponent());
	//设置 弹簧臂 长度
	SpringArm->TargetArmLength = 300.f;

	// 相机
	ViewCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ViewCamera"));
	ViewCamera->SetupAttachment(SpringArm);


	// 设置 `自动控制玩家`的值为0号玩家 值为枚举EAutoReceiveInput
	AutoPossessPlayer = EAutoReceiveInput::Player0;
}

此时蓝图已拥有弹簧臂和相机组件
image
旋转弹簧臂组件
image

如果实例的自动控制玩家无法设置,可在蓝图设置,蓝图会覆盖C++.
BP_Bird-细节面板-pawn-自动控制玩家
image

添加控制器输入 Adding Controller Input

为控制器添加旋转 FRotation

为鼠标创建轴映射,控制鸟的旋转

AController 控制器 拥有旋转,没有位置,缩放,因为不可见

image

为鼠标创建轴映射 Turn

image
轴映射也在每一帧运行

BP_Bird 蓝图中鼠标轴映射事件

image
image

添加Yaw偏航控制器,但此时不能控制旋转

绑定函数。
image

为蓝图 BP_Bird 开启 偏航

蓝图 BP_Bird -细节面板-Pawn-使用控制器旋转Yaw-勾选【默认未勾选】
image
image
此时鼠标可以旋转鸟的方向

控制台命令:show collision 显示碰撞体
image
目前胶囊体与控制器一起移动

鼠标控制俯仰角 LookUp

俯仰角。 就是抬头或低头,和大地水平面的夹角。
绑定函数。
image
image

为蓝图 BP_Bird 开启 俯仰角

绑定函数。
蓝图 BP_Bird -细节面板-Pawn-使用控制器旋转Yaw-勾选【默认未勾选】
image
此时俯仰角控制与预期相反,所以设置 轴映射-LookUp-鼠标Y-Scale 为 -1
image

C++ 中为轴映射绑定函数

E:\Unreal Projects 532\Learn\Source\Learn\Public\Pawns\Bird.h

protected:
	void MoveForward(float Value);

	// /鼠标控制偏航
	void Turn(float Value);

	// 鼠标控制俯仰角
	void LookUp(float Value);
// 将函数绑定到轴映射
void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ABird::MoveForward);
	// 偏航 Turn 与项目设置一致
	PlayerInputComponent->BindAxis(FName("Turn"), this, &ABird::Turn);
	// 俯仰角 LookUp 与项目设置一致
	PlayerInputComponent->BindAxis(FName("LookUp"), this, &ABird::LookUp);
}

void ABird::MoveForward(float Value)
{

	if (Controller && (Value != 0.f))
	{
		FVector Forward = GetActorForwardVector();
		AddMovementInput(Forward, Value);
	}
}

void ABird::Turn(float Value)
{
	AddControllerYawInput(Value);
}

void ABird::LookUp(float Value)
{
	AddControllerPitchInput(Value);
}

为蓝图 BP_Bird 的 胶囊体配置碰撞检测

BP_Bird-Capsule-细节面板-碰撞-碰撞预设
image
image
默认没有碰撞
碰撞预设-改为 BlockAll 可以碰撞
为关卡添加一个网格-碰撞预设-改为 BlockAll,用来测试碰撞。

Setting the Default Pawn 为关卡设置默认pawn

游戏默认使用项目设置的GameModeBase,其中包含默认pawn,即代表玩家的可控制的灰色圆球。
image

新建 Game Mode 蓝图

基于 Game Mode Base 新建 BP_BirdGameMode
image
打开 BP_BirdGameMode 蓝图
image
细节面板-类-默认pawn类-
image
选择 BP_Bird。
因为蓝图版本的鸟具有网格体,而C++版本的 Bird 没有网格体。
image

关卡中设置游戏模式 Game Mode

主界面-世界场景设置-游戏模式-游戏模式重载-选择 BP_BirdGameMode
image
此时关卡将不在生成默认灰色圆球pawn,而直接使用鸟。

游戏模式 Game Mode bug

使用默认游戏模式 Game Mode,在开放地图跨越多个地图区域时,会导致pawn丢失,pawn被留在之前的世界分区,未激活时卸载。导致BP_Bird也被卸载。

设置玩家出生点

将 玩家出生点 拖入关卡即可
image

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

角色类 The Character Class

https://docs.unrealengine.com/5.3/zh-CN/characters-in-unreal-engine/
添加 CharacterMovementComponent、CapsuleComponent 和 SkeletalMeshComponent 后,

Pawn类可延展为功能完善的 角色 类。 角色用于代表垂直站立的玩家,可以在场景中行走、跑动、跳跃、飞行和游泳。 此类也包含基础网络连接和输入模型的实现。
骨架网格体组件
与pawn不同的是,角色自带 SkeletalMeshComponent,可启用使用骨架的高级动画。可以将其他骨架网格体添加到角色派生的类,但这才是与角色相关的主骨架网格体。 如需了解骨架网格体的更多内容,请参见:

骨架网格体

骨架网格体动画系统
胶囊体组件
CapsuleComponent 用于运动碰撞。为了计算 CharacterMovementComponent 的复杂几何体,会假设角色类中的碰撞组件是垂直向的胶囊体。如需了解碰撞的更多信息,请参见:

胶囊体组件


角色移动组件
CharacterMovementComponent 能够使人身不使用刚体物理即可行走、跑动、飞行、坠落和游泳。 其为角色特定,无法被任何其他类实现。 可在 CharacterMovementComponent 中设置的属性包含了摔倒和行走摩擦力的值、在空气、水、土地中行进的速度、浮力、重力标度,以及角色能对物理对象施加的物理力。 CharacterMovementComponent 还包含来自动画的根运动参数, 其已转换到世界空间中,可直接用于物理。

The Character Class

从虚幻引擎-示例-导入 古代山谷 项目 Echo 人物资产

将古代山谷 的 \Content\AncientContent\Characters 目录迁移到 使用项目的 Content\ 目录

创建 Character Class

内容侧滑菜单-all-C++类-learn-Public-右键-新建C++类-LearnCharacter
image
image
image

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "LearnCharacter.generated.h"

UCLASS()
class LEARN_API ALearnCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	ALearnCharacter();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Characters/LearnCharacter.h"

// Sets default values
ALearnCharacter::ALearnCharacter()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void ALearnCharacter::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void ALearnCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void ALearnCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

角色输入 Character Inputs

为角色添加输入控制

基于 LearnCharacter 新建 BP_LearnCharacter 角色蓝图

image
image
image
角色蓝图有一个蓝色箭头-指示角色的前进方向,指向x轴
胶囊体为角色根组件,由C++创建,所以无法从继承C++的蓝图中删除,重命名。
新的场景组件将自动附加到它,附加了所有其他组件,当然,角色运动组件除外。

角色移动组件 CharacterMovementComponent
image

角色移动组件不需要附加到任何东西,因为它没有网格,它没有视觉表示,有点像控制器,它是不可见的。
它是为角色设计。
默认情况下,它具有重力,最大加速度、制动摩擦系数。

胶囊体组件
image

箭头组件
image

骨架网格体组件
image
网格体-细节面板-网格体-骨架网格体资产-选择 Echo 骨架网格体
image
image
旋转 Echo 骨架网格体 使其面向x轴,向下移动使其底部与胶囊体底部对齐
image

网格体-细节面板-动画-动画类-选择 idle动画

为角色类绑定轴映射

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "LearnCharacter.generated.h"

UCLASS()
class LEARN_API ALearnCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	ALearnCharacter();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

protected:
	void MoveForward(float Value);

	// 鼠标控制偏航
	void Turn(float Value);

	// 鼠标控制俯仰角
	void LookUp(float Value);
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

#include "Characters/LearnCharacter.h"

ALearnCharacter::ALearnCharacter()
{
	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void ALearnCharacter::BeginPlay()
{
	Super::BeginPlay();

}

// Called every frame
void ALearnCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void ALearnCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ALearnCharacter::MoveForward);
	// 偏航 Turn 与项目设置一致
	PlayerInputComponent->BindAxis(FName("Turn"), this, &ALearnCharacter::Turn);
	// 俯仰角 LookUp 与项目设置一致
	PlayerInputComponent->BindAxis(FName("LookUp"), this, &ALearnCharacter::LookUp);
}

void ALearnCharacter::MoveForward(float Value)
{
	if (Controller && (Value != 0.f))
	{
		FVector Forward = GetActorForwardVector();
		AddMovementInput(Forward, Value);
	}
}

void ALearnCharacter::Turn(float Value)
{
	AddControllerYawInput(Value);
}

void ALearnCharacter::LookUp(float Value)
{
	AddControllerPitchInput(Value);
}

更改BP_BirdGameMode的默认pawn为 BP_LearnCharacter

当前使用 BP_BirdGameMode,
打开 BP_BirdGameMode。
更改 类-默认pawn类-BP_LearnCharacter
image

设置当前关卡 GameMode

当前关卡-世界场景设置-游戏模式-游戏模式重载-BP_BirdGameMode
image

项目设置
编辑-项目设置-项目-地图和模式-默认模式-默认游戏模式-BP_BirdGameMode
image
此时可以控制角色 BP_LearnCharacter,但看不到玩家,需要设置相机

角色相机与弹簧臂 Character Camera and SpringArm

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "LearnCharacter.generated.h"

class USpringArmComponent;
class UCameraComponent;

UCLASS()
class LEARN_API ALearnCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	ALearnCharacter();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

protected:
	void MoveForward(float Value);

	// 鼠标控制偏航
	void Turn(float Value);

	// 鼠标控制俯仰角
	void LookUp(float Value);

private:
	// 弹簧臂组件
// 如果细节面板缺失,更换 SpringArm 名称为其他名称
	UPROPERTY(VisibleAnywhere)
	USpringArmComponent* SpringArm;

	// 相机组件
	UPROPERTY(VisibleAnywhere)
	UCameraComponent* ViewCamera;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

#include "Characters/LearnCharacter.h"

// 弹簧臂
#include "GameFramework/SpringArmComponent.h"
// 相机
#include "Camera/CameraComponent.h"

ALearnCharacter::ALearnCharacter()
{
	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	//弹簧臂
	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArm->SetupAttachment(GetRootComponent());
	//设置 弹簧臂 长度
	SpringArm->TargetArmLength = 300.f;

	// 相机
	ViewCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ViewCamera"));
	ViewCamera->SetupAttachment(SpringArm);


	// 设置 `自动控制玩家`的值为0号玩家 值为枚举EAutoReceiveInput
	AutoPossessPlayer = EAutoReceiveInput::Player0;
}

// Called when the game starts or when spawned
void ALearnCharacter::BeginPlay()
{
	Super::BeginPlay();

}

// Called every frame
void ALearnCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

// Called to bind functionality to input
// 将函数绑定到这些轴映射
void ALearnCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ALearnCharacter::MoveForward);
	// 偏航 Turn 与项目设置一致
	PlayerInputComponent->BindAxis(FName("Turn"), this, &ALearnCharacter::Turn);
	// 俯仰角 LookUp 与项目设置一致
	PlayerInputComponent->BindAxis(FName("LookUp"), this, &ALearnCharacter::LookUp);
}

void ALearnCharacter::MoveForward(float Value)
{
	if (Controller && (Value != 0.f))
	{
		FVector Forward = GetActorForwardVector();//获取根组件当前为胶囊体的前向向量
		AddMovementInput(Forward, Value);
	}
}

void ALearnCharacter::Turn(float Value)
{
	AddControllerYawInput(Value);
}

void ALearnCharacter::LookUp(float Value)
{
	AddControllerPitchInput(Value);
}

打开 蓝图 BP_LearnCharacter
image
调整摄像机位置角度
image
播放游戏
image
角色默认启用重力,将会从边缘跌落。

取消角色默认的控制器旋转

选中 BP_LearnCharacter-细节面板-pawn-使用控制器旋转yaw-取消勾选

image

同时,可以在C++中设置这3个属性

ALearnCharacter::ALearnCharacter()
{
	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	bUseControllerRotationPitch = false;//俯仰角 绕 y 轴的旋转
	bUseControllerRotationYaw = false;//偏航角 绕 z 轴的旋转
	bUseControllerRotationRoll = false;//翻滚角  绕 x 轴的旋转

	//弹簧臂
	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArm->SetupAttachment(GetRootComponent());
	//设置 弹簧臂 长度
	SpringArm->TargetArmLength = 300.f;

	// 相机
	ViewCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ViewCamera"));
	ViewCamera->SetupAttachment(SpringArm);


	// 设置 `自动控制玩家`的值为0号玩家 值为枚举EAutoReceiveInput
	AutoPossessPlayer = EAutoReceiveInput::Player0;
}

再不旋转根组件的情况下旋转弹簧臂

我们不希望角色继承旋转偏航俯仰,但角色相机与控制器一起移动.
选中 相机弹簧臂,弹簧臂的旋转继承自根组件即胶囊体组件,
但我们不希望胶囊体组件与控制器一起旋转。
image

选中 相机弹簧臂-细节面板-摄像机设置-继承yaw-勾选
选中 相机弹簧臂-细节面板-摄像机设置-使用pawn控制旋转-勾选
image
此时可以自由控制摄像机弹簧臂。弹簧臂与控制器一起旋转,而角色保持静止。

添加左右移动轴映射 MoveRight

image

protected:
	void MoveForward(float Value);
	void MoveRight(float Value);
void ALearnCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ALearnCharacter::MoveForward);
	PlayerInputComponent->BindAxis(FName("MoveRight"), this, &ALearnCharacter::MoveRight);
	// 偏航 Turn 与项目设置一致
	PlayerInputComponent->BindAxis(FName("Turn"), this, &ALearnCharacter::Turn);
	// 俯仰角 LookUp 与项目设置一致
	PlayerInputComponent->BindAxis(FName("LookUp"), this, &ALearnCharacter::LookUp);
}

void ALearnCharacter::MoveRight(float Value)
{
	if (Controller && (Value != 0.f))
	{
		FVector Right = GetActorRightVector();//获取根组件当前为胶囊体的右向向量
		AddMovementInput(Right, Value);
	}
}

旋转矩阵 The Rotation Matrix

6. Controller Directions

获得控制器前向向量和右向向量

void ALearnCharacter::MoveForward(float Value)
{
	if (Controller && (Value != 0.f))
	{
		// 获得控制器旋转 找到前向
		const FRotator ControlRotation = GetControlRotation();
		const FRotator yawRotation(0.f, ControlRotation.Yaw, 0.f);//偏航角旋转 绕 z 轴的旋转

		//返回控制器观察的方向向量 归一化 长度为1
		// GetUnitAxis表示获取单位向量 长度为1
		// .GetUnitAxis(EAxis::X) 获取前向向量
		// FRotationMatrix 获得旋转矩阵
		const FVector Direction = FRotationMatrix(yawRotation).GetUnitAxis(EAxis::X);//获取前向向量
		AddMovementInput(Direction, Value);
	}
}

void ALearnCharacter::MoveRight(float Value)
{
	if (Controller && (Value != 0.f))
	{
		// 获得控制器旋转 找到右向

		const FRotator ControlRotation = GetControlRotation();
		const FRotator yawRotation(0.f, ControlRotation.Yaw, 0.f);//偏航角旋转 绕 z 轴的旋转

		//返回控制器观察的方向向量 归一化 长度为1
		// GetUnitAxis表示获取单位向量 长度为1
		// .GetUnitAxis(EAxis::Y) 获取右向向量
		// FRotationMatrix 获得旋转矩阵
		const FVector Direction = FRotationMatrix(yawRotation).GetUnitAxis(EAxis::Y);//获取右向向量
		AddMovementInput(Direction, Value);
	}
}

修正角色朝向

BP_LearnCharacter蓝图-选择 角色移动组件-细节面板-角色移动(旋转设置)-将旋转朝向运动-勾选
image
image

修改角色旋转速率

BP_LearnCharacter蓝图-选择 角色移动组件-细节面板-角色移动(旋转设置)-旋转速率-z-

在c++中设置 角色移动组件 的 旋转朝向,旋转速率

// 角色移动组件
#include "GameFramework/CharacterMovementComponent.h"

ALearnCharacter::ALearnCharacter()
{
	//角色移动组件 将旋转朝向运动
	GetCharacterMovement()->bOrientRotationToMovement = true;
	//角色移动组件 旋转速率
	GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);
}

设置 头发和眉毛 Hair and Eyebrows

定位角色的网格体资源位置
BP_LearnCharacter蓝图-左侧 选择 网格体组件-细节面板-啊王个体-骨骼网格体资产-点击搜索图表
image
image
image

修饰组件 / Groom 毛发资产

Alembic(.abc)文件
Groom资产编辑器用户指南,关于如何管理属性以及编辑毛发资产的用户参考指南

https://docs.unrealengine.com/5.3/zh-CN/groom-asset-editor-user-guide-in-unreal-engine/
https://docs.unrealengine.com/5.3/en-US/API/Plugins/HairStrandsCore/UGroomComponent/

为项目包含 HairStrandsCore 模块

同时也需要包含 Niagara 模块,毛发资产依赖该模块。

E:\Unreal Projects 532\Learn\Source\Learn\Learn.Build.cs

using UnrealBuildTool;

public class Learn : ModuleRules
{
    public Learn(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Niagara", "HairStrandsCore" });

        PrivateDependencyModuleNames.AddRange(new string[] { });

        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}

添加模块后,删除项目的 Binaries Intermediate Saved 文件夹。
Learn.uproject-右键-重新生成vs项目
image

启用插件
配置模块后,必须先启用相关插件,然后编写相关代码,否则报错。
image

添加头发,眉毛 的毛发资产组件

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

class UGroomComponent;//毛发资产组件

private:
	//头发 毛发资产组件
	UPROPERTY(VisibleAnywhere, Category = "Hair")
	UGroomComponent* Hair;

	//眉毛 毛发资产组件
	UPROPERTY(VisibleAnywhere, Category = "Hair")
	UGroomComponent* Eyebrows;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

//毛发资产组件
#include "GroomComponent.h"

ALearnCharacter::ALearnCharacter()
{
	//头发毛发资产组件
	Hair = CreateDefaultSubobject<UGroomComponent>(TEXT("Hair"));
	Hair->SetupAttachment(GetMesh());//附加到角色网格体组件
	Hair->AttachmentName = FString("head");//将头发组件附加到角色网格体的head插槽名处

	//眉毛毛发资产组件
	Eyebrows = CreateDefaultSubobject<UGroomComponent>(TEXT("Eyebrows"));
	Eyebrows->SetupAttachment(GetMesh());//附加到角色网格体组件
	Eyebrows->AttachmentName = FString("head");//将头发组件附加到角色网格体的head插槽名处
}

打开 BP_LearnCharacter蓝图,可以看到头发,眉毛组件
image

分别为 头发,眉毛组件指定 -groom-groom asset

image

image

image
image

自定义发色 Custom Hair Color

打开 BP_LearnCharacter蓝图-hair 组件-细节面板-材质-元素0-点击搜索,定位材质实例
image
image
image

MI_EchoGroom-右键复制一份-为 MI_EchoGroom_Color
image
打开 MI_EchoGroom_Color 材质实例
image
更改 color-tipColor 的颜色值
更改 Pepper-pepper color 的颜色值

BP_LearnCharacter蓝图-hair 组件-细节面板-材质-元素0-选择新的材质实例 MI_EchoGroom_Color
image

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

9. 动画蓝图 The Animation Blueprint

The Animation Blueprint

制作动画蓝图

新建动画蓝图 ABP_Echo
内容侧滑菜单-右键-动画-动画蓝图
image
选择骨骼-
image
image

打开 动画蓝图 ABP_Echo
image

资产浏览器 可选择动画 添加到动画图
image
image

选中拖入的动画节点
image

细节面板-设置-循环动画-控制是否循环播放动画
image

为角色蓝图指定动画蓝图

BP_LearnCharacter蓝图-选中网格体组件-细节面板-动画-动画模式-使用蓝图动画
细节面板-动画-动画类-ABP_Echo
image
image
此时人物可以奔跑
image

改变运动

打开 ABP_Echo 动画蓝图。

左侧面板新建变量 Character ,类型为 BP_LearnCharacter,类型选择 对象引用。

软引用表示延迟加载,指向项目的本地文件路径。
对象引用本质上就像 C++ 对对象的引用,而类引用就像保存类型的变量。

image
image

这类似创建了一个空指针。
需要初始化该变量。
该动画蓝图可以获取使用该动画蓝图的骨骼网格体组件的Pawn.
因为 BP_LearnCharacter 的 骨骼网格体组件中动画的设置使用了该蓝图,
运行游戏,则存在使用该动画蓝图的实际BP_LearnCharacter对象。

事件图表

image

每次动画更新/每一帧都会调用事件 event blueprint update animation
image

在游戏开始时或拥有的 pawn 生成时调用一次 event blueprint initizlize animation
image
使用 event blueprint initizlize animation 初始化事务,例如 设置动画蓝图的 变量 Character。

try get pawn owner 返回一个当前角色BP_LearnCharacter的 pawn 类型的 对象引用.
但 变量 Character 为 BP_LearnCharacter 类型 对象引用.不可以直接设置。
使用cast,父类可以转换为子类,然后设置 变量 Character。
image
image

角色需要通过移动组件获取角色的运动状态,例如移动速度,是否在空中。

从角色 输出节点 拖出并设置 变量-角色-get Character movement

image

将 Character movement 提升为变量
image

image
image
重命名为 MovementComponent
image

image

现在根据该变量制作动画,

在 事件 event blueprint update animation 中更改动画。

从 MovementComponent 获取速度向量 get velocity
image
image

从 速度向量 获取向量大小长度 vector lengthXY

image
image

将 vector lengthXY 提升为变量 GroundSpeed 即角色的xy轴地面移动速度
image

每一帧动画都更新 GroundSpeed 变量
image

根据 GroundSpeed 播放不同动画。

使用在状态机播放多个动画。

在动画图中添加状态机
image

image
重命名为 Ground Locomotion
image
状态机 输出节点输出动画姿势数据,用来驱动角色动画。
image

编辑 Ground Locomotion 状态机,添加多个动画进行切换。

image

添加状态节点 命名为 Idle [空闲]
image
image
为 Idle [空闲] 连接一个 Run 运动状态
image
状态之间的箭头表示转换规则
image
从 Run 向 Idle 拖出一个过渡
image

将动画分配给各状态 Run,Idle

双击 Idle 进入Idle 状态,Idle 状态具由动画姿势输出节点,输出给 Ground Locomotion 状态机。
image

从右侧 资产浏览器 为 Idle 状态 拖入一个 空闲动画
image

双击Run 进入Run 状态
从右侧 资产浏览器 为 Run 状态 拖入一个 跑步动画
image

为 Ground Locomotion 状态机 设置转换规则

目前包含2个规则:
从Idle到Run,从Run到Idle.

双击顶部规则,设置 何时从Idle到Run。。
规则界面包含一个结果节点,输出参数为bool类型。
如果为true,则执行该规则,由空闲过渡到跑步。
image

GroundSpeed 大于0时返回true,过渡到跑步。
image

双击底部规则,设置 何时从Run 到 Idle。
GroundSpeed 等于0时返回true,过渡到空闲。
image

C++动画蓝图 The Anim Instance

ABP_Echo 动画蓝图右上显示了该蓝图的父类,[一个C++类]
image
动画蓝图的 类设置 选项可以设置父类
image
image

基于 AnimInstance 创建动画蓝图C++父类 LearnAnimInstance

image
image
image

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnAnimInstance.h

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "LearnAnimInstance.generated.h"

UCLASS()
class LEARN_API ULearnAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
	
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnAnimInstance.cpp

#include "Characters/LearnAnimInstance.h"

删除 ABP_Echo 动画蓝图中事件图中的 Character 变量和相关节点,只保留如下节点。
image

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnAnimInstance.h

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "LearnAnimInstance.generated.h"

UCLASS()
class LEARN_API ULearnAnimInstance : public UAnimInstance
{
	GENERATED_BODY()

public:
	virtual void NativeInitializeAnimation() override;
	virtual void NativeUpdateAnimation(float DeltaTime) override;


	UPROPERTY(BlueprintReadOnly)
	class ALearnCharacter* LearnCharacter;

	UPROPERTY(BlueprintReadOnly, Category = Movement)
	class UCharacterMovementComponent* LearnCharacterMovement;

	UPROPERTY(BlueprintReadOnly, Category = Movement)
	float GroundSpeed;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnAnimInstance.cpp

#include "Characters/LearnAnimInstance.h"
#include "Characters/LearnCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/KismetMathLibrary.h"

void ULearnAnimInstance::NativeInitializeAnimation()
{
	Super::NativeInitializeAnimation();

	LearnCharacter = Cast<ALearnCharacter>(TryGetPawnOwner());
	if (LearnCharacter)
	{
		LearnCharacterMovement = LearnCharacter->GetCharacterMovement();
	}
}

void ULearnAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
	Super::NativeUpdateAnimation(DeltaTime);

	if (LearnCharacterMovement)
	{
		GroundSpeed = UKismetMathLibrary::VSizeXY(LearnCharacterMovement->Velocity);
	}
}

打开 ABP_Echo 动画蓝图-时间图表 删除所有节点
image

更改 ABP_Echo 动画蓝图 父类为 LearnAnimInstance

类设置-类选项-父类
image
修复状态机中的变量错误,删除节点中旧的GroundSpeed,重新使用 移动分组下的GroundSpeed变量为父类的GroundSpeed 。
image
只保留 移动分组下的GroundSpeed
image

ABP_Echo 动画蓝图 显示 父类 LearnAnimInstance 的变量

image

跳跃 Jumping

创建操作映射
image

轴映射每一帧都会被执行
操作映射只在触发是执行一次,例如按下空格。
操作映射比轴映射更有效。

如果每一帧都会运行,那么轴映射就是正确的选择。
操作映射非常适合一次性动作,例如跳跃、攻击、捡起物品、扔石头。

设置 Jump 操作映射
image

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ALearnCharacter::MoveForward);
	PlayerInputComponent->BindAxis(FName("MoveRight"), this, &ALearnCharacter::MoveRight);
	// 偏航 Turn 与项目设置一致
	PlayerInputComponent->BindAxis(FName("Turn"), this, &ALearnCharacter::Turn);
	// 俯仰角 LookUp 与项目设置一致
	PlayerInputComponent->BindAxis(FName("LookUp"), this, &ALearnCharacter::LookUp);

	//绑定 Jump 操作映射
	//参数2 按下或释放
	//参数4 &ACharacter::Jump 没有重写函数,只是调用 ACharacter中已存在的函数Jump,也可以重写自己的Jump
	PlayerInputComponent->BindAction(FName("Jump"), IE_Pressed, this, &ACharacter::Jump);
}

跳跃动画 Jump Animations,添加跳跃状态机

在 父类 LearnAnimInstance 添加变量 IsFalling
E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnAnimInstance.h

public:
	UPROPERTY(BlueprintReadOnly, Category = Movement)
	bool IsFalling;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnAnimInstance.cpp

#pragma once

void ULearnAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
	Super::NativeUpdateAnimation(DeltaTime);

	if (LearnCharacterMovement)
	{
		GroundSpeed = UKismetMathLibrary::VSizeXY(LearnCharacterMovement->Velocity);
		IsFalling = LearnCharacterMovement->IsFalling();
	}
}

设置 状态机

地面 Ground Locomotion 状态机如果加入跳跃状态,状态转换越来越多,该状态机将会失控。

可以有多个状态机;
Ground Locomotion 状态机只负责地面上的运动。

创建 主状态机 Main States,
主状态将负责角色转变为不同的状态,例如,在空中.
image

设置 主状态机 Main States

添加 OnGround 状态
image

OnGround 状态 使用 Ground Locomotion 状态机缓存的姿势数据

缓存 Ground Locomotion 状态机的姿势变量为 Ground Locomotion,使其可在别处访问
image
image

在 主状态机 Main States 的 OnGround 状态 中使用 缓存的 Ground Locomotion
image
image

主状态机 Main States 添加 InAir 在空中 状态
image

进入 InAir 状态,添加 Jump_Idle_Fall 动画,并取消循环动画
image

设置 OnGround 状态 过渡到 InAir 状态 的规则
使用 IsFalling 的值。
image

在主状态机 Main States中,直接将 着陆动画 Jump_Idle_Land 拖入界面,即可新增着陆状态 Land
image
image

设置 InAir 状态 过渡到 着陆Land 状态 的规则
IsFalling 取反
image

着陆动画完成后过渡到 OnGround 状态
image

选中 着陆动画完成后过渡到 OnGround 状态 规则,设置 过渡-基于状态中序列播放器的自动规则 勾选
image
image

为 Land 到 OnGround 状态 添加第2个过渡规则
第1个过渡规则 负责转换动画状态
第2个过渡规则 负责指定何时提前过渡
image

设置第2个过渡规则,如果着陆动画播放大于0.25秒,并且地面速度大于0,则结束着陆动画。
image

image

连接 主状态机 Main States
image


IK 逆向运动学 Inverse Kinematics

使动画姿势适应非平面地形。
image
image

脚部悬空问题

image
image

逆运动学是一种来自机器人学的方法,但它在动画中也经常使用,这是一种求解方程的方法,以便我们可以移动骨骼上的特定骨骼。

在脚骨上画一个假想的球体,然后我们将那个球体推到地面上。
我们称之为球体轨迹。
将球体推到地面实际上根本不需要任何时间,它发生在单个游戏帧中,Trace 能够检测命中。
image

假设地板上的这一点在世界 Z 坐标中为零
image

另一条腿的 Z 位置为 50
image

因此,我们需要将角色推低 50 个单位,然后抬起另一条腿。

首先,我们进行球体追踪,并计算脚与其下方表面之间的偏移量。
即 ZOffset_L , ZOffset_R .其中一个将小于另一个,找出哪一个是最低的.

在这种情况下,从 Echo 的右脚到 Z 处地板的球体轨迹等于 0.
该点将具有球体轨迹的最低撞击点。
所以我们将用它来确定我们可以向下移动盆骨的偏移量。

盆骨位于臀部,但所有其他骨头都附着在它上面.
所以如果我们将骨盆向下移动,整个骨骼就会向下移动.
一旦我们将骨盆向下移动了这个量,我们的整个骨骼就会随之移动。
这意味着接触斜坡表面的腿需要向上移动。
我们需要使用逆运动学来定位该腿中的所有其他骨骼,使其自然弯曲。
当我们执行所有这些操作时,我们正在移动骨骼,如果我们立即移动它们,这可能会导致畸形,
需要使用一种称为插值的方法进行更平滑的运动。
逆运动学需要一些复杂的方程。虚幻引擎内置了 IK。所以我们不必解这些方程。
我们只需要移动那根骨头即可。称其为末端骨骼。
附加到它的所有其他骨骼将根据它一起移动。

控制绑定 Control Rig

我们将创建一个专为移动骨骼而设计的资源,这种资产称为 控制绑定 Control Rig。
内容侧滑菜单-右键 -动画-Control Rig-控制绑定

image
CR_EchoFootIK
image
image

CR_EchoFootIK 控制绑定 需要指定骨架。

Echo_Skeleton 骨骼 具由控制 IK的IK骨骼。这些骨骼没有任何实际的网格几何皮肤。
image
image

虚拟骨骼

如果没有任何 Ik 骨骼,可以使用虚拟骨骼代替。
首先需要一个虚拟根骨骼作为参考,它将采用与我们的根骨骼相同的变换。

选择根骨骼-当前为root骨骼-添加虚拟骨骼-选择root根
image
image
CB root_root 是虚拟骨骼,基于根骨骼
image

右键 CB root_root -添加虚拟骨骼-选择 foot_l
image
image

image

右键 CB root_root -添加虚拟骨骼-选择 foot_r
image

移动IK骨骼或虚拟骨骼不会影响网格体。

导入 骨骼

打开 CR_EchoFootIK
绑定层级 选项
image
import hierarchy -导入层级-选择 骨骼网格体
image
image
image
image
image

控制绑定通过改变骨骼操纵角色

制作脚部IK

在任何给定时间点,一条腿可能比另一条腿高。
要从两条腿一直向下进行球体追踪,并且球体追踪需要知道一个开始点和轨迹的终点。
选取一根脚骨(IK 或虚拟骨骼)从高点开始跟踪,在低点结束追踪。
因此,我们使用球体从脚的上方向脚下追踪一定量。需要一个函数来处理这个问题。

我的蓝图-创建函数 FootTrace
image
FootTrace将执行这条跟踪

为 FootTrace 添加输入参数 IKFootBone,类型为 绑定元素键 Rig Element Key
image
image

为 FootTrace 添加输出 HitPoint ,类型 向量 Vector
image

展开引脚
image

为左右脚执行球体追踪的蓝图函数FootTrace
image

添加一个是否显示跟踪调试的变量 ShouldDoIKTrace
image

控制图蓝图

从脚到地面的球体轨迹
image

插值使偏移平滑
image

使用较低的脚偏移设置根骨
image

将插值偏移添加到IK脚骨骼或虚拟骨骼,使其向上或向下移动。
image

全身IK 确保移动脚时使腿部自然弯曲
image

整体图
image

将 CR_EchoFootIK 控制绑定的 变量 ShouldDoIKTrace 设置为公开
image

动画蓝图添加控制绑定

打开 ABP_Echo 动画蓝图
添加 Control Rig
image
为其设置 控制绑定类

选中 Control Rig 细节面板-Control Rig-控制绑定类-CR_EchoFootIK
image

启用 ShouldDoIKTrace
image

设置 ShouldDoIKTrace

当角色不在空中时才进行追踪
image

奔跑时,不应使用IK
如果地面速度为零,则混合姿势使用True节点输入的IK动画,否则使用false节点输入的非IK动画。
缓存 主状态机备用
image

最终效果
image
image
image
image

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

10. 碰撞和重叠 Collision and Overlaps

碰撞预设Collision Presets

为关卡拖入一块岩石静态网格体
image

该实例是静态网格物体actor。
image
并且它有一个静态网格物体组件,并且该静态网格物体组件自动设置了自己的静态网格物体。
image
image
一拖入一个自动静态网格物体 Actor,就会创建一个自动静态网格物体 Actor,因为它具有静态网格组件,组件具有碰撞设置,我们可以在详细信息面板中看到这些设置。

静态网格组件碰撞预设
image

任何能够碰撞的组件都有一个 碰撞预设Collision Presets,定义碰撞行为
打开BP_LearnCharacter蓝图
选择 网格体 组件-细节面板-碰撞-碰撞预设
image

碰撞预设-对象类型Object Type

选择岩石实例-细节面板--碰撞-碰撞预设-自定义custom
image

所有发生碰撞的组件都有自己的对象类型来确定其他组件如何与该对象类型碰撞交互.
物理体是一种对象类型.
image

碰撞预设-碰撞响应

包含 Collision Responses检测响应和Object Responses物体响应.
它们中的每一个都是碰撞对象类型.
在右侧我们可以选择忽略、重叠或阻止该对象类型。
如果世界上的一个组件具有特定的对象类型,那么所有其他组件都将具有他们各自对该对象类型的响应。

在此示例中,我们正在查看对象类型为PhysicsBody物理体的组件的碰撞预设.
因此其他actor可以决定忽略、重叠或阻止对象类型为PhysicsBody物理体的actor。

碰撞对象类型允许我们自定义碰撞行为。

碰撞预设-碰撞已启用

碰撞已启用选项:
1无碰撞
2纯查询(无物理碰撞)
3纯物理(不查询碰撞)
4已启用碰撞(查询和物理)
5仅探测(接触数据,无查询或物理碰撞 )
6查询和探测(查询碰撞和接触数据,无物理碰撞)

无碰撞

不在物理引擎中创建任何代表。无法用于空间查询(光线投射、扫描、重叠)或模拟(刚体、约束。能达成的最佳性能(尤其对于移动物体而言)

该组件将完全穿过物体或其他物体将穿过它不会有任何响应.不会互相干扰。
不需要物体从它们或类似的东西上反弹

纯查询(无物理碰撞)

只用于空间查询(光线投射、扫描、重叠)。无法用于模拟(刚体、约束)。适用于角色运动和不需要物理模拟的内容。让数据停留在模拟树外可提升性能。

光线投射是当我们在控制绑定中执行某种跟踪时,我们为每只脚执行球体跟踪,向地面绘制一个假想的球体,并从中得到击中结果,让我们知道这次撞击的位置。这是一个空间查询。

扫描是指对象检测它们是否即将彼此重叠。例如胶囊组件。

重叠是指两个对象实际上彼此重叠的情况。

有多种方法可以检测这些事件何时发生,但我们必须启用空间查询。

纯查询意味着不进行物理模拟。虚幻引擎有一个高性能物理引擎chaos。

纯物理(不查询碰撞)

只用于物理模拟(刚体、约束)。无法用于空间查询(光线投射、扫描、重叠)。适用于不需要逐骨骼检测的角色上的闪烁位元。
不让数据进入查询树可以提升性能。

当两个模拟物理的物体碰撞时,这允许诸如重力和力之类的东西互相作用在这些物体上,并且它们会相互反弹。

纯物理意味着如果我们执行光线投射则没有查询,扫描。这些东西不会被检测到,因为该组件只是模拟物理。
因此物理引擎正在该组件上工作,这意味着可以模拟重力并向其他物理对象施加力。

已启用碰撞(查询和物理)

可用于空间查询(光线投射、扫描、重叠)和模拟(刚体、约束)
这是两全其美的。
可以进行光线投射和扫描等跟踪。
可以有重力,我们可以有碰撞。
这是最昂贵的,通常用于我们希望能够推动的物体或扔东西。也能够对诸如追踪之类的东西做出反应。例如控制绑定追踪。

仅探测(接触数据,无查询或物理碰撞 )

仅用于探测物理模拟(刚体、约束)。不能用于空间查询(光线投射扫描、重叠)。当你想要检测潜在的物理交互帮传递接触数据到命中回调或接触修改但不想对这些接触做出物理反应时,此方法非常实用。

查询和探测(查询碰撞和接触数据,无物理碰撞)

可以用于空间查询(光线投射、扫描、重叠和探测物理模拟(刚体、约束)将不允许实际的物理交互,但将生成接触数据,触发命中回调并且接触将出现在接触修改中

示例

WorldStatic + 无碰撞

更改岩石的 碰撞预设-碰撞已启用 为 无碰撞,对象类型 为WorldStatic

角色将穿过岩石,无碰撞,无追踪,角色IK追踪失效。
image
image

WorldStatic + 纯查询(无物理碰撞)

角色将穿过岩石,无碰撞,有追踪,角色IK追踪正常。
image

显示碰撞体积命令 播放时-按下波浪键 ~ ,输入 show collision

image
角色的碰撞胶囊体与岩石重叠。
胶囊使用空间查询,所以我们希望我们的胶囊被阻止以免与岩石重叠。

岩石没有碰撞组件
右键 岩石 实例-浏览至资产-双击打开 岩石 静态网格体
显示-简单碰撞-将看不到简单碰撞网格。【因为该网格没有简单碰撞】
image
image

显示-复杂碰撞-
image
取消显示

添加 碰撞-添加球体简化碰撞

image
image
此时角色将无法穿过岩石,角色与岩石都具有碰撞体积。
image
image
角色可站立在岩石上
image
角色脚IK空间查询追踪与岩石碰撞体重叠,但角色胶囊碰撞体与岩石碰撞体不重叠。
image

打开岩石网格体-显示简单碰撞,移除碰撞

image

碰撞-添加26DOP简单碰撞 【贴合岩石外形】
image
image

WorldStatic + 纯物理(不查询碰撞)

角色与岩石重叠,角色IK追踪也失效。
只有当物体具有空间查询时,我们的胶囊体才能阻止我们穿过物体。
物理不能阻挡物体重叠,只能使用查询阻挡物体重叠。
image

WorldStatic + 已启用碰撞(查询和物理)

角色与岩石的碰撞体积不重叠,角色IK追踪正常。
image

选择 岩石 静态网格体组件-细节面板-物理-模拟物理-启用勾选
image
角色将可以撞击移动岩石【地板静态网格体也必须使用已启用碰撞(查询和物理)】
image
这是因为物理引擎允许我们对其施加力。
胶囊正在使用空间查询来阻止事物并阻止我们穿过他们。

由于角色的胶囊体组件已启用碰撞(查询和物理),碰撞预设为 Pawn.能够将物理力传递到其他模拟物理的对象上
image

岩石默认启用了重力,所以会坠落。如果取消重力,岩石将会漂浮,但依然有物理模拟,可以被撞击移动。
image
image

碰撞响应

碰撞响应 具由多行选项
细节面板-碰撞-碰撞预设-碰撞响应
image

BP_LearnCharacter 角色-胶囊体组件-细节面板-碰撞-碰撞预设-对象类型-Pawn
image

设置 岩石实例-静态网格体-细节面板-碰撞-碰撞预设-碰撞响应-物体相应-Pawn-勾选 忽略项
image
角色与岩石的查询正常,IK追踪正常。但角色胶囊体碰撞体积与岩石碰撞体重叠。角色胶囊体没有被岩石阻挡。
image

如果将岩石改为 -碰撞已启用 -纯物理(不查询碰撞)
角色腿部将完全穿过岩石,因为角色IK查询追踪也失效。只有查询可以阻挡物体碰撞体积之间的重叠。

image
此时相机穿过岩石没有自动变焦效果,需要为岩石启用查询。

为岩石启用 已启用碰撞(查询和物理)
相机经过岩石时会自动变焦跳过岩石

为岩石启用空间查询,仅针对脚部IK追踪生效,但不对相机生效。

设置 岩石实例-静态网格体-细节面板-碰撞-碰撞预设-碰撞响应-检测响应-camera-勾选 忽略 项
image
即使岩石启用了查询,它也会忽略相机碰撞通道,这意味着我们没有得到相机放大效果。

例如制作密室暗门,
将迷失墙壁设置为pawn玩家忽略查询,可以通过墙壁,但相机使用查询与墙壁碰撞,不可以通过。除非跟随角色穿墙。

重叠事件 Overlap Events

重叠时触发事件,
目标是Actor。

打开蓝图 BP_Item-事件图- 可以看到重叠事件,表示与其他actor重叠时执行该函数。
image
此actor与另一个actor重叠时调用的事件。例如玩家走进触发器时。如需了解对象拥有阻挡碰撞的事件(如玩家与墙壁发生碰撞),请查看“命中"事件。
Note::在这个和其他actor上组件的bGenerateOverlapEvents都必须设为true,才能生成重叠事件。

image

BP_Item 默认的碰撞预设为 blockAllDynamic,阻挡全部类型的的对象,角色也无法穿过BP_Item,不能触发BP_Item的重叠事件。
image
image
BP_Item 只对 pawn 碰撞对象类型重叠感兴趣。

BP_LearnCharacter 角色-胶囊体的碰撞预设为 Pawn,
已启用碰撞(查询和物理).

image

检测响应-Visibility-忽略 【控制绑定追踪】

BP_Item 设置对pawn的重叠

BP_Item -碰撞-碰撞预设-cusetom
BP_Item -碰撞-碰撞预设-碰撞响应-物体响应-pawn-勾选 重叠
image
因此可以使用空间查询,并且可以触发BP_Item 与 pawn的重叠事件
image

此时相机缩放影响视角

测试 StaticMesh球体重叠事件

为BP_Item添加静态网格体组件StaticMesh
image
为StaticMesh选择一个静态网格体【圆球 】
image
image

设置 StaticMesh-碰撞预设-custom
StaticMesh-碰撞预设-碰撞响应-物体响应-pawn-勾选 重叠
image
此时与 StaticMesh球体重叠即可触发重叠事件。
删除StaticMesh球体。

不与BP_Item网格本身重叠,而是与BP_Item周围的一般区域重叠,不包括BP_Item网格。

为 BP_Item 添加 球体碰撞sphere collision
image
image

sphere collision 碰撞预设默认设置为重叠所有动态。
仅具有查询,因此球体组件没有物理特性,但启用了空间查询。
并且它设置为与所有其他对象类型重叠。
所以我们可以继续与球体重叠。触发BP_Item的重叠事件。
image

image
image
image

显示 BP_Item 的 sphere collision
BP_Item-sphere collision组件-细节面板--渲染-游戏中隐藏-取消勾选
image
image

可以将 sphere collision 拖离 原网格体
image
image

on component begin overlap

假设只希望在与sphere collision本身而不是 BP_Item金字塔重叠时生成该事件。
为 sphere collision 组件本身设置重叠事件
选择 BP_Item- sphere collision-添加事件- on component begin overlap
image
image
事件名称括号的sphere表示只与该球体重叠才触发事件
image
如果我们与金字塔重叠,什么也不会发生。
但如果我们与sphere collision球体本身重叠,我们的消息就会打印到屏幕上。
它为我们提供了更多的控制权。
这对于拾取世界上的拾取器非常有用。一旦我们真正拾取这些拾取器,这些拾取器就会被销毁。

overlapped component 输出 碰撞体,例如 BP_Item- sphere collision
other actor 输出与 sphere collision重叠的对象,例如角色名
other comp 输出与 sphere collision重叠的对象的组件 例如角色的胶囊体碰撞体组件

事件代理 Delegates

虚幻引擎有一个复杂的系统来实现观察者模式。
委托是一种特殊类型的类,能够存储观察者列表。
当游戏中发生事件时,主体有能力向每个观察者进行广播。
这涉及循环委托列表并调用观察者对象上的所有回调。

On Component Begin Overlap

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Item.h

class USphereComponent;

private:
	//  sphere collision
	UPROPERTY(VisibleAnywhere)
	USphereComponent* Sphere;

protected:
	//动态多播 事件代理
	//FHitResult为传出的重叠结果 const 使其不被外界修改
	//UFUNCTION 确保函数可以被绑定到委托
	UFUNCTION()
	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	UFUNCTION()
	virtual void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Components/SphereComponent.h"

void AItem::BeginPlay()
{
	Super::BeginPlay();

	// 将 OnSphereOverlap 绑定到当前sphere collision委托
	// 此时一切都已初始化完成
	//参数1 为用户对象指针
	Sphere->OnComponentBeginOverlap.AddDynamic(this, &AItem::OnSphereOverlap);
	Sphere->OnComponentEndOverlap.AddDynamic(this, &AItem::OnSphereEndOverlap);
}

void AItem::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	//如果有对象与此sphere collision重叠时,将调用此函数
	//委托将循环遍历其委托列表并调用绑定到该委托的任何回调函数。现在我们还没有绑定它。
	//当委托调用此函数时,它将传递有关该重叠事件的信息。可以看到另一个演员是什么,另一个组件是什么。
	const FString otherActorName = OtherActor->GetName();
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(1, 30.f, FColor::Red, otherActorName);
	}

}

void AItem::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	const FString otherActorName = FString("EndOverlap ") + OtherActor->GetName();
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(1, 30.f, FColor::Blue, otherActorName);
	}
}

此时 BP_Item 蓝图中将拥有 Sphere 组件,缩放其大小
image

当前物体item mesh碰撞相机时,相机显示不够自然。

打开 BP_Item 蓝图-item mesh 组件-碰撞-碰撞预设-custom

-碰撞-碰撞预设-碰撞响应-检测相应-camera-勾选 忽略 选项
image
image

准备待拾取武器

打开 BP_Item 蓝图-item mesh 组件-静态网格体-静态网格体 选择一把剑
image

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

11. The Weapon Class

从 Item 类派生武器类

image

actor类包含了放置在世界中的能力。
我们基于 actor 类派生了一个称为 item 的子类,并且添加了我们自己的自定义它的功能。
过创建一个武器类来进一步子类化,并且武器类可以继承item的属性,但随后继续专门化其自己的行为。
该物品能够以正弦运动上下浮动,这样武器就有了很好的浮动效果。
同时具由重叠事件。
如果我们创建一个名为 Weapon 的项目子类,它将继承这些属性。
现在,除了这些功能之外,我还希望能够装备该武器。
将该武器附加到角色上,本质上将网格附加到手上。

如果角色装备了武器,我们应该能够使用该武器。与世界互动、攻击事物并进行战斗。

创建一个新的 C++ 武器类

内容侧滑菜单-all-C++类-Learn-Public-Items-item 右键-创建派生自Item的C++类 Weapon
image
image
E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

#pragma once

#include "CoreMinimal.h"
#include "Items/Item.h"
#include "Weapon.generated.h"


UCLASS()
class LEARN_API AWeapon : public AItem
{
	GENERATED_BODY()

public:
	AWeapon();
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

#include "Items/Weapons/Weapon.h"

AWeapon::AWeapon()
{

}

右键 Weapon 类-创建基于Weapon的蓝图类 BP_Weapon
image
image

缩放武器使其大小合适 BP_Weapon-Item Mesh-变换-缩放

从BP_Item 事件图表复制波动函数
image

修改Item类,添加 virtual 前缀 使重叠事件可被子类覆盖,{虚函数} ,可在武器类中重写该函数
E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Item.h

protected:
	//动态多播 事件代理
	//FHitResult为传出的重叠结果 const 使其不被外界修改
	//UFUNCTION 确保函数可以被绑定到委托
	UFUNCTION()
	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	UFUNCTION()
	virtual void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

在武器类中 添加override 后缀重写重叠函数

继承重写的函数和属性不需要 UFUNCTION()
E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

#pragma once

#include "CoreMinimal.h"
#include "Items/Item.h"
#include "Weapon.generated.h"


UCLASS()
class LEARN_API AWeapon : public AItem
{
	GENERATED_BODY()

public:
	AWeapon();

protected:
	//  继承重写的函数和属性不需要 UFUNCTION() 宏,该宏已经隐式继承
	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;

	virtual void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) override;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

#include "Items/Weapons/Weapon.h"

AWeapon::AWeapon()
{

}

void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	Super::OnSphereOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);
}

void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	Super::OnSphereEndOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex);

}

插槽 Sockets

将 actor 附件到角色骨骼插槽处

定位到 BP_LearnCharacter 角色骨骼网格体文件
网格体-骨骼网格体资产
image
打开 echo 骨骼网格体
image
切换到骨骼树
image

在骨骼 hand_r 上添加一个插槽,重命名为 RightHandSocket
image
image
image
插槽可以连接物体,插槽会和附着的骨骼一起移动。

为插槽添加预览网格体

RightHandSocket 插槽-右键-添加预览资产-选择剑的网格体
image
image

选择 RightHandSocket 插槽 ,调正其位置
image

右侧-动画-预览控制器-使用特定动画 选择一个动画预览效果
image
image
通过下方时间可查看特定位置动画

下载动画资产 Downloading Animations

https://www.mixamo.com/#/
使用adobe账号登陆

下载角色

搜索 xbot 角色
image

点击下载,fbx,t-pose
image
文件:X Bot.fbx

内容侧滑菜单-导入-选择 X Bot.fbx

该角色文件自身具备骨骼,所以不需要选择骨骼,保持默认空。

image
点击导入。
忽略平滑组警告。
image

查看物理资产 X_Bot_PhysicsAsset

image
image

下载动画序列

在动画栏搜索 axe
下载standing idle空闲动画
image
with skin 意味着我们将下载网格和动画数据,但我们可以选择不使用,之前下载了角色文件已经拥有的皮肤。
使用 without skin 选项。
image
将 Standing Idle 动画文件放置到动画文件夹,与之前的角色文件分开。

继续下载动画 standin melee attack.

内容侧滑菜单-导入-选择2个动画-网格体-骨骼 选择之前xbo角色自带的 SKM_Bot 骨骼
image
image
image
导入。

image

IK Rig【IK绑定】 为动画重定向创建 IK Rig

https://docs.unrealengine.com/5.0/zh-CN/ik-rig-in-unreal-engine/

必须下载 Maximo 骨骼网格体【角色自带】是有原因的,因为它是 为这些Maximo 骨骼动画使用的。

打开动画序列。
在动画编辑器的资源详细信息中,我们看到骨架并注意到它呈灰色。
image
这是因为动画是特定于使用它们的骨骼的。

echo 与 Maximo的骨骼树完全不同。必须通过IK绑定获得Maximo骨骼树。
在虚幻引擎中,为了使一个骨架的动画在另一个骨架上工作,我们执行一种称为重定向的操作,用于将动画从一个骨架重定向到另一个骨架。

可以为骨架网格体创建 IK绑定,用骨骼链将它们映射到另一个骨架网格体的IK绑定。

创建 IK绑定

内容测出啊菜单-右键-动画-重定向-IK绑定 [IK_XBot]
image
打开 IK_XBot,右侧选择 SK_Bot 骨骼网格体
image

image
骨骼 Hips 是当前 Maximo 骨骼树的根骨骼。臀骨。位于顶层。所有其他骨骼都附着在该骨骼上。

骨骼 Hips是重定向root.

image

选中 Hips -右键-设置重定向根
image
image
身体的每个部分都是由骨头链组成的。
如果创建一个新的骨骼链,指定哪些骨骼属于该链,那么一旦创建了一个Echo 的 IK rig,我们可以为 Echo 选择相应的骨骼并为它们创建一个链。这就是我们如何从一个骨架映射到另一个骨架并获取动画数据以进行回显的方法。所以我们这里的工作是创建链并选择每个链中应包含哪些骨骼。

Root 链

每个IK绑定都需要一个根链。
Maximo 骨骼的根链由一根骨头组成,即我们的臀部骨头Hips。
因此,我们可以右键单击臀部并从选定的骨骼中选择新的重定向链。
选中 Hips -右键-新建重定向链
image
image
这个菜单,要求我们命名链并提供起始骨骼和结束骨骼。
命名为 Root.骨骼不变。
image
添加链。
image

脊柱骨骼链 Spine

顺序选择 3个spine 骨骼 -右键-新建重定向链 Spine
image
之后新建Echo的IK绑定的所有链名必须与此相同。
image

颈部头部链 Head

顺序选择 Neck,Head骨骼 -右键-新建重定向链 Head
image
image

左肩 LeftClavicle

左肩为单骨骼链。
选择 LeftShoulder 骨骼 -右键-新建重定向链 LeftClavicle
image
image

右肩 RightClavicle

右肩为单骨骼链。
选择 RightShoulder 骨骼 -右键-新建重定向链 RightClavicle
image
image

左手到左臂 LeftArm

顺序选择 LeftArm,LeftForeArm,LeftHand 骨骼 -右键-新建重定向链 LeftArm
image
image

右手到右臂 RightArm

顺序选择 RightArm,RightForeArm,RightHand 骨骼 -右键-新建重定向链 RightArm
image
image

左手大拇指 LeftThumb

顺序选择左手大拇指1-4,4个骨骼 LeftHandThumb1, LeftHandThumb2, LeftHandThumb3, LeftHandThumb4 骨骼 -右键-新建重定向链 LeftThumb

image
image

左手食指 LeftIndex

顺序选择左手食指1-4,4个骨骼 LeftHandIndex1, LeftHandIndex2, LeftHandIndex3, LeftHandIndex4 骨骼 -右键-新建重定向链 LeftIndex
image
image

左手中指 LeftMiddle

顺序选择中手食指1-4,4个骨骼 LeftHandMiddle1, LeftHandMiddle2, LeftHandMiddle3, LeftHandMiddle4 骨骼 -右键-新建重定向链 LeftMiddle

image
image

左手无名指指 LeftRing

顺序选择中手食指1-4,4个骨骼 LeftHandRing1, LeftHandRing2, LeftHandRing3, LeftHandRing4 骨骼 -右键-新建重定向链 LeftRing
image
image

左手小拇指 LeftPinky

顺序选择中手食指1-4,4个骨骼 LeftHandPinky1, LeftHandPinky2, LeftHandPinky3, LeftHandPinky4 骨骼 -右键-新建重定向链 LeftPinky
image
image

右手同理

左腿 LeftLeg

顺序选择 LeftUpLeg,LeftLeg,LeftFoot,LeftToeBase,LeftToe_End 骨骼 -右键-新建重定向链 LeftLeg
image
image

右腿 RightLeg

顺序选择 RightUpLeg,RightLeg,RightFoot,RightToeBase,RightToe_End 骨骼 -右键-新建重定向链 RightLeg
image
image

Maximo 的IK绑定完成
image


制作 Echo IK绑定

一旦完成,就可以将基于 Maximo 骨架的任何动画传输到 Echo 并使用。

新建 IK 绑定 IK_Echo
image
选择 echo 骨骼网格体
image
这里有很多我们的IK绑定中不需要的骨头。
我们只需要与我们在 Maximo IK绑定中制作的链条相对应的链条。

注意Maximo 骨架没有根骨。【高亮部分】,但Echo骨架有根骨root。
image
果我们选择根骨骼,我们会看到这根骨骼从腿之间开始,一直延伸到盆骨,盆骨实际上是与Maximo臀骨相对应的骨头.

这里也需要一个重定向根,但不是 root骨骼,而是盆骨pelvis 。这与之前的盆骨对应。
image

Echo 设置重定向根

盆骨pelvis - 右键-设置重定向根
image
image

###Echo 的 Root 链
现在,就像我们在 Maximo IK绑定中所做的那样,我们将拥有一条带有单根骨骼的链,称为Root.
只是这一次我们实际上要使用root骨骼而不是盆骨pelvis。

选中 root -右键-新建重定向链 Root [链名称与Maximo中的链相同]
image
image

Echo 的 脊柱骨骼链 Spine

Echo 上的脊柱有五根骨头,Maximo有3根。
Echo 脊柱链将由这五块骨头组成。
顺序选择 5个spine 骨骼 -spine_01,spine_02,spine_03,spine_04,spine_05,-右键-新建重定向链 Spine
image
image

Echo 的 颈部头部链 Head

顺序选择 neck_01,neck_02,head骨骼 -右键-新建重定向链 Head
image
image

Echo 的 左肩 LeftClavicle

左肩为单骨骼链。
选择 clavicle_l 骨骼 -右键-新建重定向链 LeftClavicle
image
image

Echo 的 右肩 RightClavicle

右肩为单骨骼链。
选择 clavicle_r 骨骼 -右键-新建重定向链 RightClavicle
image
image

Echo 的 左手到左臂 LeftArm

顺序选择 upperarm_l,lowerarm_l,hand_l 骨骼 -右键-新建重定向链 LeftArm
image
image

Echo 的 右手到右臂 RightArm

顺序选择 upperarm_r,lowerarm_r,hand_r 骨骼 -右键-新建重定向链 RightArm
image
image

Echo 的 左手大拇指 LeftThumb

顺序选择左手大拇指1-3,3个骨骼 thumb_01_l, thumb_02_l, thumb_03_l骨骼 -右键-新建重定向链 LeftThumb
image
image

Echo 的 左手食指 LeftIndex

这与 Maximo 版本也有点不同.
我们不能使用这些掌骨,否则我们在重定向动画时会遇到一些问题。
所以我们要做的只是选择手指骨骼。
不选择 index_metacarpal_l 这样的位置与Maximo不一致的掌骨,只选择位置与Maximo一致指骨。
所有的其他手指的掌骨都不选择。

顺序选择左手食指1-3,3个骨骼 index_01_l, index_02_l, index_03_l 骨骼 -右键-新建重定向链 LeftIndex
image
image

Echo 的 左手中指 LeftMiddle

顺序选择中手食指1-3,3个骨骼middle_01_l,middle_02_l ,middle_03_l 骨骼 -右键-新建重定向链 LeftMiddle
image

Echo 的 左手无名指指 LeftRing

顺序选择中手食指1-3,3个骨骼 ring_01_l ,ring_02_l,ring_03_l骨骼 -右键-新建重定向链 LeftRing

Echo 的 左手小拇指 LeftPinky

顺序选择中手食指1-3,3个骨骼 pinky_01_l ,pinky_02_l,pinky_03_l 骨骼 -右键-新建重定向链 LeftPinky

Echo 的 右手同理

Echo 的 左腿 LeftLeg

顺序选择 thigh_l,calf_l,calf_twist_02_l,calf_twist_01_l,foot_l,ball_l 骨骼 -右键-新建重定向链 LeftLeg
image
这将出现扭曲的骨头,扭曲的骨头会破坏链条。
将导致添加了3条链
image
将 链 LeftLeg 的 末端骨骼指定为 ball_l,
image
删除另外2条多余的链
image

Echo 的 右腿 RightLeg

同左腿

Echo 的IK绑定完成
image
image
image

IK重定向 IK Retargeter

将Mixamo动画重定向至Echo

为Echo 新建 IK重定向器 RTG_XBot

内容侧滑菜单-右键-动画-重定向-IK重定向器
image
image
打开 RTG_XBot

细节面板-源-源IKRig资产-选择 IK_XBot IK绑定
细节面板-目标-目标IKRig资产-选择 IK_Echo IK绑定
image
设置 目标网格体偏移 ,使两个网格体不再重叠
image
image

当前他们的姿势不同。
xbot处于T姿势,Echo是A姿势.
如果我们像这样重新定位动画,我们可能会得到不太理想的结果。

可以通过转到资产浏览器来查看重定向动画的样子.
双击相应的动画。
image

编辑姿势

到顶部点击-编辑重定向姿势
image
角色-骨骼-所有层级

我们会突然看到所有的骨头,并且我们可以移动它们。
现在骨骼本身缩放太大,这是因为在细节中设置了骨骼绘制尺寸。
在编辑姿势模式下,我们可以更改骨骼的姿势,实际上我们可以更改姿势,将其保存为姿势。

创建-新建
image
image
image
现在就有了这两个echo姿势。
如果我们愿意,我们可以切换回默认姿势,并且可以在其中保存多个姿势.为不同目的重新定位资产。

选择 -编辑重定向姿势-
image

选择上臂骨骼并将其向上移动。调整使echo姿势与xbot相同。
image

播放动画查看效果。

重新向动画

选择 需要的 动画,点击 导出选定动画
image
image
双击 动画序列 预览
image
此处可以修改插槽位置。
单击时间轴上方可暂停,调整动画时间。

附加剑 Attaching the Sword

附加剑到右手插槽

蓝图插槽

修改 ItemMesh 属性使其蓝图可读
E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Item.h

public:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	UStaticMeshComponent* ItemMesh;

	//  sphere collision
	UPROPERTY(VisibleAnywhere,  BlueprintReadOnly)
	USphereComponent* Sphere;

打开 BP_Weapon 蓝图
断开 波动函数
image

选择 Sphere 组件后,在事件图添加事件 On Component Begin Overlap (Sphere)
目标为 Sphere组件。
image

尝试将 Other Actor 转换为 BP_LearnCharacter,cast to BP_LearnCharacter
image

武器网格体实际存在于 BP_Weapon 的 Item Mesh 组件。从中取出。
使用 attach component to component [将组件附加到组件]将武器网格体从 Item Mesh 组件 取出,附加到 BP_LearnCharacter的 Mesh组件RightHandSocket 插槽上。
对于变换规则,全部使用 snap to target 对齐到目标 ,将武器贴合插槽。而非使用相对偏移。
最后一个选项表示是否模拟物理。

image

此时角色靠近武器时可以拾取剑
image
image
如果拾取剑较小,可在缩放插槽【待优化】

C++ 拾取武器

打开 Weapon 类

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

#include "Items/Weapons/Weapon.h"
#include "Characters/LearnCharacter.h"

AWeapon::AWeapon()
{

}

void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	Super::OnSphereOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);

	ALearnCharacter* LearnCharacter = Cast<ALearnCharacter>(OtherActor);
	if (LearnCharacter)
	{
		FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
		ItemMesh->AttachToComponent(LearnCharacter->GetMesh(), TransformRules, FName("RightHandSocket"));
	}
}

void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	Super::OnSphereEndOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex);

}

拾取物体 Picking Up Items

创建操作映射以拾取物体

添加操作Equip 映射按键 E
image

Item

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

#include "Items/Item.h"
#include "Learn/DebugMacros.h"
#include "Components/SphereComponent.h"
#include "Characters/LearnCharacter.h"

AItem::AItem()
{
	PrimaryActorTick.bCanEverTick = true;

	ItemMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ItemMeshComponent"));
	RootComponent = ItemMesh;

	Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
	Sphere->SetupAttachment(GetRootComponent());
}

void AItem::BeginPlay()
{
	Super::BeginPlay();
	Sphere->OnComponentBeginOverlap.AddDynamic(this, &AItem::OnSphereOverlap);
	Sphere->OnComponentEndOverlap.AddDynamic(this, &AItem::OnSphereEndOverlap);
}

float AItem::TransformedSin()
{
	return Amplitude * FMath::Sin(RunningTime * TimeConstant);
}

float AItem::TransformedCos()
{
	return Amplitude * FMath::Cos(RunningTime * TimeConstant);
}

void AItem::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	ALearnCharacter* LearnCharacter = Cast<ALearnCharacter>(OtherActor);
	if (LearnCharacter)
	{
		// 如果当前物体与角色重叠,将调用角色的设置物体方法,并将自身传进去
		LearnCharacter->SetOverlappingItem(this);
	}
}

void AItem::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	ALearnCharacter* LearnCharacter = Cast<ALearnCharacter>(OtherActor);
	if (LearnCharacter)
	{
		// 如果当前物体与角色重叠结束,将调用角色的设置物体方法,传空指针,不指向任何内容
		LearnCharacter->SetOverlappingItem(nullptr);
	}
}

void AItem::Tick(float DeltaTime)
{
	RunningTime += DeltaTime;
	Super::Tick(DeltaTime);
}

Weapon

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

#pragma once
#include "CoreMinimal.h"
#include "Items/Item.h"
#include "Weapon.generated.h"

UCLASS()
class LEARN_API AWeapon : public AItem
{
	GENERATED_BODY()

public:
	AWeapon();
	void Equip(USceneComponent* InParent,FName InSocketName);
protected:
	//  继承重写的函数和属性不需要 UFUNCTION() 宏,该宏已经隐式继承
	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;

	virtual void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) override;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

#include "Items/Weapons/Weapon.h"
#include "Characters/LearnCharacter.h"

AWeapon::AWeapon()
{

}

void AWeapon::Equip(USceneComponent* InParent, FName InSocketName)
{
	FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
	ItemMesh->AttachToComponent(InParent, TransformRules, InSocketName);
}

void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	Super::OnSphereOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);
}

void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	Super::OnSphereEndOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex);

}

LearnCharacter

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "LearnCharacter.generated.h"

class USpringArmComponent;
class UCameraComponent;
class UGroomComponent;//毛发资产组件
class AItem;

UCLASS()
class LEARN_API ALearnCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ALearnCharacter();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

protected:
	void MoveForward(float Value);
	void MoveRight(float Value);

	// 鼠标控制偏航
	void Turn(float Value);

	// 鼠标控制俯仰角
	void LookUp(float Value);

protected:
	// 拾取物体
	void EKeyPressed();

private:
	UPROPERTY(VisibleAnywhere)
	USpringArmComponent* SpringArm;

	UPROPERTY(VisibleAnywhere)
	UCameraComponent* ViewCamera;

private:
	UPROPERTY(VisibleAnywhere, Category = "Hair")
	UGroomComponent* Hair;

	UPROPERTY(VisibleAnywhere, Category = "Hair")
	UGroomComponent* Eyebrows;

private:
	UPROPERTY(VisibleInstanceOnly)
	AItem* OverlappingItem;

public:
	// 将简单的setter函数内联提高编译速度
	// 运行时将直接执行,无需跳转到定义
	// FORCEINLINE 是虚幻提供的强制内联宏
	FORCEINLINE void SetOverlappingItem(AItem* Item) { OverlappingItem = Item; }
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

#pragma once
#include "Characters/LearnCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GroomComponent.h"
#include "Items/Item.h"
#include "Items/Weapons/Weapon.h"

ALearnCharacter::ALearnCharacter()
{
	PrimaryActorTick.bCanEverTick = true;

	bUseControllerRotationPitch = false;//俯仰角 绕 y 轴的旋转
	bUseControllerRotationYaw = false;//偏航角 绕 z 轴的旋转
	bUseControllerRotationRoll = false;//翻滚角  绕 x 轴的旋转

	//角色移动组件 将旋转朝向运动
	GetCharacterMovement()->bOrientRotationToMovement = true;
	//角色移动组件 旋转速率
	GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);

	//弹簧臂
	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArm->SetupAttachment(GetRootComponent());
	//设置 弹簧臂 长度
	SpringArm->TargetArmLength = 300.f;

	// 相机
	ViewCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ViewCamera"));
	ViewCamera->SetupAttachment(SpringArm);

	// 设置 `自动控制玩家`的值为0号玩家 值为枚举EAutoReceiveInput
	AutoPossessPlayer = EAutoReceiveInput::Player0;

	//头发毛发资产组件
	Hair = CreateDefaultSubobject<UGroomComponent>(TEXT("Hair"));
	Hair->SetupAttachment(GetMesh());//附加到角色网格体组件
	Hair->AttachmentName = FString("head");//将头发组件附加到角色网格体的head插槽名处

	Eyebrows = CreateDefaultSubobject<UGroomComponent>(TEXT("Eyebrows"));
	Eyebrows->SetupAttachment(GetMesh());//附加到角色网格体组件
	Eyebrows->AttachmentName = FString("head");//将头发组件附加到角色网格体的head插槽名处
}

void ALearnCharacter::BeginPlay()
{
	Super::BeginPlay();

}

void ALearnCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ALearnCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ALearnCharacter::MoveForward);
	PlayerInputComponent->BindAxis(FName("MoveRight"), this, &ALearnCharacter::MoveRight);
	PlayerInputComponent->BindAxis(FName("Turn"), this, &ALearnCharacter::Turn);
	PlayerInputComponent->BindAxis(FName("LookUp"), this, &ALearnCharacter::LookUp);
	PlayerInputComponent->BindAction(FName("Jump"), IE_Pressed, this, &ACharacter::Jump);
	PlayerInputComponent->BindAction(FName("Equip"), IE_Pressed, this, &ALearnCharacter::EKeyPressed);
}

void ALearnCharacter::MoveForward(float Value)
{
	if (Controller && (Value != 0.f))
	{
		const FRotator ControlRotation = GetControlRotation();
		const FRotator yawRotation(0.f, ControlRotation.Yaw, 0.f);//偏航角旋转 绕 z 轴的旋转
		const FVector Direction = FRotationMatrix(yawRotation).GetUnitAxis(EAxis::X);//获取前向向量
		AddMovementInput(Direction, Value);
	}
}

void ALearnCharacter::MoveRight(float Value)
{
	if (Controller && (Value != 0.f))
	{
		const FRotator ControlRotation = GetControlRotation();
		const FRotator yawRotation(0.f, ControlRotation.Yaw, 0.f);//偏航角旋转 绕 z 轴的旋转
		const FVector Direction = FRotationMatrix(yawRotation).GetUnitAxis(EAxis::Y);//获取右向向量
		AddMovementInput(Direction, Value);
	}
}

void ALearnCharacter::Turn(float Value)
{
	AddControllerYawInput(Value);
}

void ALearnCharacter::LookUp(float Value)
{
	AddControllerPitchInput(Value);
}

void ALearnCharacter::EKeyPressed()
{
	AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem);
	if (OverlappingWeapon)
	{
		OverlappingWeapon->Equip(GetMesh(), FName("RightHandSocket"));
	}
}

此时,在靠近武器时,按E可以拿起武器。

角色状态枚举 Enum for Character State

打开 ABP_Echo 动画蓝图
ground locomotion - idle 状态-替换新的待机动画为IK重定向后的 Standing_Idle

image

添加是否持有武器变量,在空闲状态下,使动画蓝图在未持有武器空闲姿态和持有武器空闲姿态之间切换。
因此需要 角色状态。

使用 VS添加枚举头文件

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\CharacterTypes.h

#pragma once

// 虚幻枚举必须E开头
// class 表示作用域枚举,使用枚举时必须完全限定
// 默认第一个枚举常量值为0,但可以覆盖.ECS_Unequipped = 0
// 虚幻引擎通常为枚举添加前缀,使用枚举名缩写,当前为 ECS
// uint8 将枚举值限定为8位整数
// UENUM(BlueprintType) 使蓝图可访问该枚举
// UMETA(DisplayName = "Unequipped"), 表示在蓝图显示的名称
UENUM(BlueprintType)
enum class ECharacterState : uint8
{
	ECS_Unequipped UMETA(DisplayName = "Unequipped"),
	ECS_EquippedOneHandedWeapon UMETA(DisplayName = "Equipped One-Handed Weapon"),
	ECS_EquippedTwoHandedWeapon UMETA(DisplayName = "Equipped Two-Handed Weapon")
};

LearnCharacter

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

#include "CharacterTypes.h"

private:
	ECharacterState CharacterState = ECharacterState::ECS_Unequipped;//使用了类型的值

public:
	// 将简单的setter函数内联提高编译速度
	// 运行时将直接执行,无需跳转到定义
	// FORCEINLINE 是虚幻提供的强制内联宏
	FORCEINLINE ECharacterState GetCharacterState() const { return CharacterState; }//const 表示该函数不能改变该类的任何东西

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::EKeyPressed()
{
	AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem);
	if (OverlappingWeapon)
	{
		OverlappingWeapon->Equip(GetMesh(), FName("RightHandSocket"));
		//装备武器后 更新状态角色状态
		CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
	}
}

需要在动画蓝图 LearnAnimInstance 中使用角色状态以切换姿势
E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnAnimInstance.h

#include "CharacterTypes.h"

public:
	UPROPERTY(BlueprintReadOnly, Category = Movement)
	ECharacterState CharacterState;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnAnimInstance.cpp

void ULearnAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
	Super::NativeUpdateAnimation(DeltaTime);

	if (LearnCharacterMovement)
	{
		GroundSpeed = UKismetMathLibrary::VSizeXY(LearnCharacterMovement->Velocity);
		IsFalling = LearnCharacterMovement->IsFalling();
		CharacterState = LearnCharacter->GetCharacterState();//从 角色获取角色状态保存
	}
}

基于角色状态切换动画蓝图姿势 Switching Animation Poses

打开 ABP_Echo 动画蓝图
ground locomotion - idle 状态-替换新的待机动画为IK重定向后的 Standing_Idle
此时,点击 显示继承的变量, 可访问继承自C++的 CharacterState 变量
image
image

添加 blend poses 节点【基于ECharacterState】。
它需要一个枚举值,我们可以获取字符状态并将其插入。
混合姿势可以检查角色状态的值,并且可以根据需要返回不同的姿势。
右键点击 blend poses-default pose 添加 新的状态 Unequipped pose,
再添加一个 Equipped One-Handed Weapon
image

将 Idle 动画 链接 Unequipped pose,
将 Standing_Idle 动画 链接 Equipped One-Handed Weapon.
复制一个 Idle 动画 链接 Default pose,放置人物进入 A-pose

blend poses 节点将检查字符状态的值。

blend poses 节点的 default blend time 表示不同动画姿势之间的转化融合过渡时间
image

装备武器时的跑步动画 Equipped Animations

https://www.mixamo.com/#/ 搜索 axe 动画
选择 standing run forward
该角色动画实际上是在向前奔跑,而我们不希望这样。
应该在游戏中自己控制角色的移动。不希望动画推动角色前进。
所以非常重要的是我们在这里选中 in place 复选框,就像我们的其他动画一样原地运行。
image

点击下载,然后选择不带皮肤,因为我们已经有了骨骼网格体。我们不需要皮肤,我们只需要动画数据。
image

导入 Standing Run Forward.fbx
由于不自带骨骼,因此选择 mixamo 骨骼 SK_xbot。[之前命名错误,骨骼应使用 SK前缀,骨骼网格体使用SKM]
image
image

IK重定向该动画

打开 RTG_XBot IK重定向器
image
image

选择 standing run forward 导出选定动画
image
image

ABP_Echo 动画蓝图添加持剑奔跑动画

打开 ABP_Echo 动画蓝图 ground locomotion -run 状态.
添加 standing run forward 动画序列【勾选循环动画】。
添加 blend poses 节点【基于ECharacterState】。
添加 CharacterState 变量
image

注意

如果我们继续这种简单地将混合姿势节点添加到所有适用的动画蓝图中。
这将需要大量工作。而且它的可扩展性也不是很好。
不应该用一张失控的巨大动画蓝图。
应该有多个动画蓝图,并且知道如何根据正在发生的情况在这些动画蓝图之间进行切换。

多个动画蓝图 Multiple Animation Blueprints

链接多个动画蓝图

地面运动状态机和主状态机应该位于它们自己的动画蓝图中

ABP_Echo_MainStates

新建动画蓝图 ABP_Echo_MainStates,骨骼选择 Echo_Skeleton

image
image

从 ABP_Echo 复制两个状态机
image

如果编辑器卡住,须在 VS编辑中点击继续
image

编译 ABP_Echo_MainStates,将显示警告的变量 Character State 右键-创建为新变量

image
image
再次编译-警告消失。
根据编译器结果,找到有问题的变量
image

Is Falling ,ground speed 变量同理。

image

将 Main States 状态机输出
image
image

ABP_Echo 中链接 ABP_Echo_MainStates

打开 ABP_Echo。右键添加 linked anim graph 节点
image
linked anim graph 节点-细节面板-设置-实例类-ABP_Echo_MainStates 动画蓝图
image
image
这将 ABP_Echo_MainStates 动画蓝图 连接到该 ABP_Echo 动画蓝图中使用。

在 ABP_Echo 中 为 ABP_Echo_MainStates 绑定变量

打开 ABP_Echo,直接将 Character State 变量拖入 linked anim graph【ABP_Echo_MainStates】可以为其绑定变量。
或者 选中 linked anim graph【ABP_Echo_MainStates】-设置-exposable properties-选择变量
image
image

将 linked anim graph【ABP_Echo_MainStates】 缓存给 Main States

image

删除其他未使用的空节点。最终如下
image

ABP__Echo_IK 动画蓝图

新建 ABP__Echo_IK 动画蓝图,骨骼选择 Echo_Skeleton。

ABP__Echo

打开 ABP__Echo 动画蓝图,添加 linked anim graph【ABP_Echo_IK】节点
image
将 ABP__Echo 动画蓝图 的IK控制相关节点全部复制到 ABP__Echo_IK 动画蓝图

image
image

ABP__Echo_IK

ABP__Echo_IK 中删除缓存节点 Main States.
将 Is Falling,Ground Speed 创建变量
image
新建 input pose 节点
image
选中 input pose 节点细节面板-输入-名称-重命名为 MainStates
image

ABP__Echo

在 ABP__Echo 选择 linked anim graph【ABP_Echo_IK】,右键-刷新节点-可以看到in pose
image
image

将缓存 Main States 连接到 linked anim graph【ABP_Echo_IK】输入处。
image
将 linked anim graph【ABP_Echo_IK】 连接到主输出
image

ABP__Echo_IK

将 input pose 节点 缓存为 Main States
use cache Main States 分配给 control rig 和 blend pose by bool 的 false.
image

ABP__Echo

为 linked anim graph【ABP_Echo_IK】绑定变量 Is Falling,Ground Speed 。
image

删除多余的IK控制节点。最终如下
image

主蓝图动画 ABP__Echo 只负责为子蓝图动画传递变量。组织子蓝图动画。

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

攻击 Attacking

Animation Montages

创建攻击动画蒙太奇

状态机是处理动画状态的好方法,因为这些条件会被不断检查,一旦地速大于零,我们就会立即看到姿态的变化。
但对于某些游戏机制,不适合使用状态机,而是使用动画蒙太奇响应游戏中发生的事件的一次性动画。
可以将蒙太奇视为一个或多个动画的一种容器。
例如,我们可能有一个名为“攻击蒙太奇”的蒙太奇,其中包含两个攻击动画。
在蒙太奇中,我们可以创建与蒙太奇中的动画相关的部分,然后通过函数调用来播放蒙太奇,该函数调用指定要播放的蒙太奇以及要跳转到的部分。这对于攻击等一次性动作非常有用。
image

攻击动作映射

首先,创建一个新的攻击操作映射。使用鼠标左键。
如果我们要进行操作映射,这意味着我们需要回调,例如攻击函数,我们可以将其绑定到我们的攻击操作映射。
这意味着当我们点击鼠标左键时,我们的攻击函数就会被调用。
攻击函数中会播放动画蒙太奇,为此我们必须访问动画实例。
从敌人实例中,我们可以播放动画蒙太奇,指定要播放哪个蒙太奇。
image

设置一个Attack操作映射,以便我们可以按下按钮并获得响应中调用的函数。

编辑项目设置,创建攻击操作映射。
image

蓝图攻击函数

打开 BP_LearnCharacter
右键单击并输入Attack,这是我们在制作攻击操作映射后立即创建的操作事件。
image
image

制作一个动画蒙太奇并向其中添加一些攻击动画

https://www.mixamo.com/ 动画选项 搜索 axe ,
下载 standing melee attack,standing melee attack 360 动画。均不使用皮肤。

下载 unarmed run forward 。不使用皮肤。勾选 in place。
导入3个动画,骨骼选择 SK_Bot [mixamo骨骼]
image
image

重定向动画

打开 RTG_XBot IK重定向器
image
image

选择新的3个动画-导出选定动画
image
image

创建 动画蒙太奇 AM_AttackMontage

右键-动画-动画蒙太奇-选择骨骼-Echo_Skeleton
image
image
AM_AttackMontage-资产浏览器-将 动画 Standing_Melee_Attack_Horizontal 拖入 时间轴
image

蒙太奇现在包含一个动画,实际上可以包含多个动画。
拖入 Standing_Melee_Attack_360_High 至 defaultFroup
image
现在动画蒙太奇可以有多个部分。可以将蒙太奇分段并命名每个部分。

右键 -新建蒙太奇片段 Attack1
image
将 Default 片段拖离删除,将Attack1片段拖至动画1开头。
image
右键 -新建蒙太奇片段 Attack2,拖至动画2开头.
image

打开 蒙太奇片段
image
点击清除,分开片段,放置全部播放。
image

插槽

蒙太奇可以与称为插槽的东西相关联,插槽只不过是一个标记或标签。
默认情况下,当您创建动画蒙太奇时,它会被分配到默认插槽 DefaultGroup.DefaultSlot。
image

如果我们尝试播放与给定插槽相关的蒙太奇,我们必须让我们的动画蓝图知道我们正在使用该插槽。

打开 ABP_Echo 动画蓝图,从 ABP_Echo_IK 节点输出拖出 slot[DefaultSlot]节点。并且该节点被分配到默认槽位。
可以创建具有不同名称的自定义插槽,稍后我们将这样做。
image
image

现在我们只需让我们的姿势通过默认插槽运行就足够了。
它会覆盖我们角色的姿势。

播放攻击动画

动画蓝图是一个原子实例,可以通过角色访问动画实例。
打开 BP_LearnCharacter 事件图
拖入 角色 Mesh 组件,从Mesh组件获取动画实例get anin instance。访问动画实例的 函数 montage play.

image

Montage Play 允许我们选择要播放的蒙太奇。可以从下拉列表中选择一项。选择 AM_AttackMontage
image

Montage Play 有许多输入,包括播放速率。可以选择更快或更慢地播放动画,将其保留为以常规速度播放。

return value type :默认情况下,Montage Play返回蒙太奇的长度。

in time to start montage at :Montage Play还可以指定开始的时间点。

stop all montages:意味着当这个函数被调用时,如果我们已经在播放其他的蒙太奇,它会停止那些并播放当前这个。

所以当按下鼠标左键时,这个输入动作将响应。我们将从网格中获取 Adam 实例,并调用蒙太奇播放来指定新的攻击蒙太奇。
现在我们没有指定蒙太奇部分,因此它将默认播放第一个蒙太奇部分 Attack1。
现在可以按鼠标左键,现在 Echo 会播放攻击动画。
image

可以从动画实例get anin instance 拖出 montage jump 跳转播放蒙太奇片段 节点,输入片段 Attack2
image

section name:输入片段 Attack2

montage:选择资产 AM_AttackMontage
image

当按下鼠标左键时,将播放Attack2.

C++ 播放动画蒙太奇 Playing Montages from C++

创建一个回调函数来绑定到我们的攻击操作映射

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

class UAnimMontage;//蒙太奇动画

protected:
	void Attack();//攻击 绑定到攻击操作映射Attack

private:
	// 将其暴露给蓝图,可以从角色蓝图中选择动画蒙太奇。
	// 能够从角色的默认蓝图进行设置
	UPROPERTY(EditDefaultsOnly, Category = "Montages")
	UAnimMontage* AttackMontage; // 攻击蒙太奇动画

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

#include "Animation/AnimMontage.h"

void ALearnCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAction(FName("Attack"), IE_Pressed, this, &ALearnCharacter::Attack);//攻击 绑定到攻击操作映射Attack
}

void ALearnCharacter::Attack()
{
	//播放蒙太奇
	//需要访问动画实例,我们可以通过角色 MASH 来获取它。
	//GetMesh 将返回我们的骨骼网格体组件,并且从骨骼网格体中我们可以使用称为获取动画实例的公共 getter 函数 GetAnimInstance。

	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && AttackMontage)
	{
		// 播放蒙太奇动画
		// 后续参数默认
		// AttackMontage 蒙太奇动画 必须在蓝图中设置
		// 播放 AttackMontage 动画蒙太奇的片段 1
		AnimInstance->Montage_Play(AttackMontage);
		//随机播放AttackMontage中的蒙太奇片段
		const int32 Selection = FMath::RandRange(0, 1);//0或1
		FName SectionName = FName();//片段名

		switch (Selection)
		{
		case 0:
			SectionName = FName("Attack1");
			break;//跳出switch
		case 1:
			SectionName = FName("Attack2");
			break;//跳出switch
		default:
			break;
		}
		// 播放 AttackMontage 动画蒙太奇的片段 SectionName
		AnimInstance->Montage_JumpToSection(SectionName, AttackMontage);
	}
}

打开 蓝图 BP_LearnCharacter ,细节面板-设置-montages-Attack Montage-选择 AM_AttackMontage 动画蒙太奇
image
image

角色攻击状态 Attacking State

更换跑步武器动画
https://www.mixamo.com/ 动画栏,搜索 sword run,下载 动画 sword and shield run ,不包含皮肤,勾选 in place。

导入动画,选择 SK_Bot【mixamo】骨骼.
使用 RTG_XBot IK重定向器导出该动画。
可以在RTG_XBot IK重定向器中新建RunPose修正跑步姿势后,再导出动画。
image

替换 ABP_MainStates 动画蓝图的跑步动画

打开 ABP_MainStates -ground locomotion -run 状态机
勾选循环动画。
image

修复重复攻击

如果我们已经在攻击,就不能攻击。
要决定是否应该能够攻击,我们需要跟踪角色的状态。

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\CharacterTypes.h

UENUM(BlueprintType)
enum class EActionState : uint8
{
	EAS_Unoccupied UMETA(DisplayName = "Unoccupied"),
	EAS_HitReaction UMETA(DisplayName = "HitReaction"),
	EAS_Attacking UMETA(DisplayName = "Attacking"),
	EAS_EquippingWeapon UMETA(DisplayName = "Equipping Weapon"),
	EAS_Dodge UMETA(DisplayName = "Dodge"),
	EAS_Dead UMETA(DisplayName = "Dead")
};

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

protected:
	void PlayAttackMontage();//播放蒙太奇动画

private:
	EActionState ActionState = EActionState::EAS_Unoccupied;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::Attack()
{
	if (ActionState == EActionState::EAS_Unoccupied)
	{
		PlayAttackMontage();
		ActionState = EActionState::EAS_Attacking;
	}

}

void ALearnCharacter::PlayAttackMontage()
{
	//播放蒙太奇
	//需要访问动画实例,我们可以通过角色 MASH 来获取它。
	//GetMesh 将返回我们的骨骼网格体组件,并且从骨骼网格体中我们可以使用称为获取动画实例的公共 getter 函数 GetAnimInstance。

	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && AttackMontage)
	{
		// 播放蒙太奇动画
		// 后续参数默认
		// AttackMontage 蒙太奇动画 必须在蓝图中设置
		// 播放 AttackMontage 动画蒙太奇的片段 1
		AnimInstance->Montage_Play(AttackMontage);
		//随机播放AttackMontage中的蒙太奇片段
		const int32 Selection = FMath::RandRange(0, 1);//0或1
		FName SectionName = FName();//片段名

		switch (Selection)
		{
		case 0:
			SectionName = FName("Attack1");
			break;//跳出switch
		case 1:
			SectionName = FName("Attack2");
			break;//跳出switch
		default:
			break;
		}
		// 播放 AttackMontage 动画蒙太奇的片段 SectionName
		AnimInstance->Montage_JumpToSection(SectionName, AttackMontage);
	}
}

使用动画通知重置动作状态 Resetting the Action State

在攻击动画完成后立即进行攻击。

AM_AttackMontage

打开攻击蒙太奇 AM_AttackMontage
在攻击蒙太奇中,我们有两个攻击部分。
当攻击动画结束时。将该ActionState行动状态重置回空闲状态,然后我们就可以再次攻击。
image

再 通知-1 右键 -添加通知-新建通知
image
将通知 AttackEnd 放置dao到片段1的末尾,但不能靠近片段2.
image
当播放此动画部分时到达此攻击结束并通知。
可以从角色动画蓝图中访问它。

通过右上角-更多-进入主动画蓝图ABP_Echo
image

ABP_Echo -事件图

右键 添加 事件 AnimNotify_AttackEnd
image
image
攻击动画播放到结尾将触发该事件。重置动作状态。

meta=(AllowPrivateAccess="true" 将私有变量暴露给蓝图,是蓝图可编辑该变量

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

private:
	// meta=(AllowPrivateAccess="true" 将私有变量暴露给蓝图,是蓝图可编辑该变量
	UPROPERTY(BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
	EActionState ActionState = EActionState::EAS_Unoccupied;

ABP_Echo -事件图-获取 角色类-从中获取 ActionState 设置为 未装备状态
image

AM_AttackMontage

打开攻击蒙太奇 AM_AttackMontage,为片段添加通知 AttackEnd.
image

在C++中响应动画通知

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

protected:
	// 响应动画通知
	UFUNCTION(BlueprintCallable)
	void AttackEnd();

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

//响应动画结束通知
void ALearnCharacter::AttackEnd()
{
	ActionState = EActionState::EAS_Unoccupied;
}

ABP_Echo -事件图-获取 角色类-转化为有效的get-从中调用方法AttackEnd() 设置为 未装备状态
image
从 LearnCharacter 输出节点拖出 调用函数 AttackEnd
image
image

在没有武器时不能攻击

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

protected:
	bool CanAttack();//验证可攻击条件

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::Attack()
{
	if (CanAttack())
	{
		PlayAttackMontage();
		ActionState = EActionState::EAS_Attacking;
	}

}

bool ALearnCharacter::CanAttack()
{
	return ActionState == EActionState::EAS_Unoccupied &&
		CharacterState != ECharacterState::ECS_Unequipped;
}

物体枚举状态 Item State

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Item.h

enum class EItemState : uint8
{
	EIS_Hovering,
	EIS_Equipped
};

protected:
	// 物体默认状态为悬浮状态
	EItemState ItemState = EItemState::EIS_Hovering;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Item.cpp

void AItem::Tick(float DeltaTime)
{
	RunningTime += DeltaTime;
	Super::Tick(DeltaTime);

	//物体悬浮
	if (ItemState == EItemState::EIS_Hovering)
	{
		AddActorWorldOffset(FVector(0.f, 0.f, TransformedSin()));
	}
}

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

void AWeapon::Equip(USceneComponent* InParent, FName InSocketName)
{
	FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
	ItemMesh->AttachToComponent(InParent, TransformRules, InSocketName);
	ItemState = EItemState::EIS_Equipped;//设为悬浮状态
}

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::MoveForward(float Value)
{
	if (ActionState != EActionState::EAS_Unoccupied) return;//只有空闲状态可以移动
	if (Controller && (Value != 0.f))
	{
		const FRotator ControlRotation = GetControlRotation();
		const FRotator yawRotation(0.f, ControlRotation.Yaw, 0.f);//偏航角旋转 绕 z 轴的旋转
		const FVector Direction = FRotationMatrix(yawRotation).GetUnitAxis(EAxis::X);//获取前向向量
		AddMovementInput(Direction, Value);
	}
}

声音通知和元声音 Sound Notifies and Meta Sounds

为动画添加声音

导入 wav声效
image

为动画序列添加声效动画通知[UE4版本]

打开 Standing_Melee_Attack_360_High 动画序列
将时间轴移动到剑挥出的地方
image

在标签栏1-右键-添加通知-播放音效
image
image

选中该 playsound-细节面板-动画通知-音效-选择-whoos音效
image
image
播放动画可听到音效
删除该音效。

为动画蒙太奇添加音效[UE4版本]

打开 AM_AttackMontage
在轨道1定位武器挥出时机,右键-添加通知-播放音效

image

image
选中该 playsound-细节面板-动画通知-音效-选择-whoos音效
image

添加新轨道 whoosh,将音效移动到该轨道
image
image

复制音效 whoosh,移动到动画片段2.

image

双击音效文件可修改音效属性,但会影响使用该音效的2个动画片段。所以虚幻有了sound cue
image
image

sound cue [UE4版本]

右键-音频-ssound cue
SC_Whoosh
image
image
打开 SC_Whoosh
image
将Whoosh音频拖入该界面,连接到输出即可。
image

打开 AM_AttackMontage-选中该 playsound-细节面板-动画通知-音效-选择-SC_Whoosh
image

MetaSound源 [UE5]

首先启用 MetaSound 插件
image

右键 -新建-MetaSound源
sfx_Whoosh

image
image
打开 sfx_Whoosh
image
image

点击-输入 加号,添加输入
image
命名为Whoosh
image

类型选择-WaveAsset
image

将输入 Whoosh 拖入面板
右键添加-声波播放器 Wave Player 用以播放输入
image
image

为 输入 Whoosh 选择 默认值-默认-whoosh 音波
image

使音调随机化 音高频移偏移

右键添加 random float 随机(浮点),音高在0-1之间随机。连接 音高频移偏移
image

使音量随机化 输出声道

右键添加 random float 随机(浮点),音高在0-1之间随机。与 输出声道 相乘

image

设置动画蒙太奇声音

打开 AM_AttackMontage-选中该 playsound-细节面板-动画通知-音效-选择-sfx_Whoosh
image
image

脚步声 MetaSound 声音数组随机

新建 MetaSound ,sfx_Exert
新建输入 Exert
image

类型为-WaveAsset-勾选为数组
image

默认值-默认-添加10个不是同的音波
image
每次都会随机播放其中之一。

在面板拖入输入 Exert,
右键添加-shuffle(WaveAsset:Array)
shuffle输出声波,需要声波播放器
右键-添加-wave player
image

添加 2个random 使音调,音高随机
image

为动画蒙太奇添加挥舞呼吸声

打开 AM_AttackMontage-新建Exert轨道,添加2个sfc_Exert 音效
image

为动画添加脚步声 Meta Sounds for Footsteps

新建 MetaSound ,sfx_Rock_Run
新建输入 RockRun,
类型为-WaveAsset-勾选为数组.

选中多个音效,直接拖入 RockRun面板- 默认值-默认

image
image

勾选 shuffle 的 启用共享状态,将优化共享数据,避免复制。
image

image

为动画序列添加通知

打开动画序列 Sword_And_Shield_Run
添加音轨 RockRun_L ,RockRun_R,
在脚着地时添加通知-动画通知-音效-选择-sfx_Rock_Run
image

修复脚部位置 Fixing Foot Placement

设置初始IK骨骼位置【罗圈腿】
打开 控制绑定 CR_EchoFootIK
预览场景-动画-预览控制器-使用特定动画-Standing_Idle

image
姿势为罗圈腿
image

这与IK骨骼有关。

image

断开 向前解析 后,腿部正常,IK骨骼没有跟随腿部。
image
解决这个问题的方法是我们可以对脚骨进行变换并使用该位置来设置。

从 foot_l获取平移,来设置ik_foot_l
右键 transforms-get transform
右键 transforms-set transform

image
现在 ik_foot_l 骨骼连接到了 真实的foot_l 脚骨根部

设置右脚
image

连接到执行序列,修复罗圈腿。
image
image

收起剑 Putting the Sword Away /武装和解除武装 arming and disarming

www.mixamo.com
在动画栏-搜索 axe 。
下载动画 standing disarn over shoulder 收剑动画。unarmed equid over shoulder拔剑动画。不需要皮肤,【依旧是xbot角色】
导入动画,选择 SK_Bot 骨骼

image

重命名为 Equip,Unequip

打开 RTG_XBot IK重定向器 ,导出这2个动画。
image

制作装备武器Equip蒙太奇动画

选中 动画序列 Equip-右键-创建-创建动画蒙太奇-AM_Equip
image

打开 AM_Equip 动画蒙太奇-资产浏览器,右侧将 Unequip动画序列拖入时间轴到片段2
image

新建片段 Equip,Unequip。
蒙太奇片段-清除。
image

C++ 装备/解除蒙太奇

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

class AWeapon;

protected:
	void PlayEquipMontage(FName SectionName);//收剑或拿剑
	bool CanDisarm();//是否可以解除武装
	bool CanArm();//是否可以抽剑

private:
	UPROPERTY(EditDefaultsOnly, Category = "Montages")
	UAnimMontage* EquipMontage; // 装备与解除 蒙太奇动画

	UPROPERTY(VisibleAnywhere, Category = "Weapon")
	AWeapon* EquippedWeapon;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::EKeyPressed()
{
	AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem);
	if (OverlappingWeapon)
	{
		OverlappingWeapon->Equip(GetMesh(), FName("RightHandSocket"));
		//装备武器后 更新状态角色状态
		CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
		OverlappingItem = nullptr;
		EquippedWeapon = OverlappingWeapon;
	}
	else {
		//如果不是在拾取武器,那么开始装备武器
		if (CanDisarm()) {
			PlayEquipMontage(FName("Unequip"));
			CharacterState = ECharacterState::ECS_Unequipped;
		}
		else if (CanArm()) {
			PlayEquipMontage(FName("Equip"));
			CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
		}
	}
}

bool ALearnCharacter::CanDisarm()
{
	return ActionState == EActionState::EAS_Unoccupied &&
		CharacterState != ECharacterState::ECS_Unequipped &&
		EquipMontage;
}

bool ALearnCharacter::CanArm()
{
	return ActionState == EActionState::EAS_Unoccupied &&
		CharacterState == ECharacterState::ECS_Unequipped &&
		EquippedWeapon;
}

播放武装和解除武装动画 Playing Arm and Disarm Animations

打开 BP_LearnCharacter 蓝图,-细节面板-Montages-Equip Montage-选择 AM_Equip 动画蒙太奇
image
image

使用骨骼插槽将剑装在背后 Attaching the Sword to the Back

打开 BP_LearnCharacter 蓝图 ,选择 网格体 Mesh组件,浏览至骨骼网格体资产
image

打开 Echo 骨骼网格体
右键-spine_05骨骼-添加插槽
SpineSocket
image

SpineSocket 插槽-右键-添加预览资产-dag

image

调整插槽的位置,通过预览动画 equip。
image

为 AM_Equip动画蒙太奇 添加收剑,拿剑动画通知

当剑到后被位置时-右键-添加通知-新建通知

image

C++

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

public:
	void AttachMeshToSocket(USceneComponent* InParent, const FName& InSocketName);

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

void AWeapon::Equip(USceneComponent* InParent, FName InSocketName)
{
	AttachMeshToSocket(InParent, InSocketName);
	ItemState = EItemState::EIS_Equipped;//设为悬浮状态
}

void AWeapon::AttachMeshToSocket(USceneComponent* InParent, const FName& InSocketName)
{
	FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
	ItemMesh->AttachToComponent(InParent, TransformRules, InSocketName);
}

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

protected:
	//收剑
	UFUNCTION(BlueprintCallable)
	void Disarm();

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::Disarm()
{
	if (EquippedWeapon)
	{
		EquippedWeapon->AttachMeshToSocket(GetMesh(), FName("SpineSocket"));
	}
}

响应收剑,收剑通知时间

打开 ABP_Echo动画蓝图 -事件图-调用C++的Disarm()

image

拿剑

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

protected:
	//拿剑
	UFUNCTION(BlueprintCallable)
	void Arm();

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::Arm()
{
	if (EquippedWeapon)
	{
		EquippedWeapon->AttachMeshToSocket(GetMesh(), FName("RightHandSocket"));
	}
}

响应拿剑,拿剑通知事件

打开 ABP_Echo动画蓝图 -事件图-调用C++的Arm()
image

修复装备时滑动行走

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::MoveRight(float Value)
{
	if (ActionState != EActionState::EAS_Unoccupied) return;//只有空闲状态可以移动
	if (Controller && (Value != 0.f))
	{
		const FRotator ControlRotation = GetControlRotation();
		const FRotator yawRotation(0.f, ControlRotation.Yaw, 0.f);//偏航角旋转 绕 z 轴的旋转
		const FVector Direction = FRotationMatrix(yawRotation).GetUnitAxis(EAxis::Y);//获取右向向量
		AddMovementInput(Direction, Value);
	}
}



void ALearnCharacter::EKeyPressed()
{
	AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem);
	if (OverlappingWeapon)
	{
		OverlappingWeapon->Equip(GetMesh(), FName("RightHandSocket"));
		//装备武器后 更新状态角色状态
		CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
		OverlappingItem = nullptr;
		EquippedWeapon = OverlappingWeapon;
	}
	else {
		//如果不是在拾取武器,那么开始装备武器
		if (CanDisarm()) {
			PlayEquipMontage(FName("Unequip"));
			CharacterState = ECharacterState::ECS_Unequipped;
			ActionState = EActionState::EAS_EquippingWeapon;
		}
		else if (CanArm()) {
			PlayEquipMontage(FName("Equip"));
			CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
			ActionState = EActionState::EAS_EquippingWeapon;
		}
	}
}

AM_Equip 动画蒙太奇 添加动画通知

收剑或拿剑动画结束时,添加 FinishEquippin 通知,将状态改为
image

C++ FinishEquipping

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

protected:
	//收剑,拿剑动作结束
	UFUNCTION(BlueprintCallable)
	void FinishEquipping();

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::FinishEquipping()
{
	ActionState = EActionState::EAS_Unoccupied;
}

响应拿剑/收剑完成,通知事件

打开 ABP_Echo动画蓝图 -事件图-调用C++的FinishEquipping()

image

装备和取消装备声音 Equip and Unequip Sounds

打开 AM_Equip 蒙太奇动画,新建 EquipSound轨道,在抽剑放剑处新建播放音效 PlaySound

第一次拾取剑的声音

新建 MetaSound源 sfx_Shink
image

C++

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

class USoundBase;

private:
	// 在蓝图中设置抽剑声效,在拾取武器时播放
	UPROPERTY(EditAnywhere, Category = "Weapon Properties")
	USoundBase* EquipSound;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

#include "Kismet/GameplayStatics.h"//播放声音

void AWeapon::Equip(USceneComponent* InParent, FName InSocketName)
{
	AttachMeshToSocket(InParent, InSocketName);
	ItemState = EItemState::EIS_Equipped;//设为悬浮状态

	if (EquipSound)//如果设置了声效
	{
		UGameplayStatics::PlaySoundAtLocation(
			this,//世界上下文对象
			EquipSound,//声效
			GetActorLocation()//播放声音的位置
		);
	}
}

为 BP_Weapon 设置声效资源EquipSound 为 sfx_Shink

image
image

修复拾取武器后依然触发武器重叠事件

拾取武器后应设置碰撞预设为 无碰撞

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Item.h

protected:
	//  sphere collision
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	USphereComponent* Sphere;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

#include "Components/SphereComponent.h"

void AWeapon::Equip(USceneComponent* InParent, FName InSocketName)
{
	AttachMeshToSocket(InParent, InSocketName);
	ItemState = EItemState::EIS_Equipped;//设为悬浮状态

	if (EquipSound)//如果设置了声效
	{
		UGameplayStatics::PlaySoundAtLocation(
			this,//世界上下文对象
			EquipSound,//声效
			GetActorLocation()//播放声音的位置
		);
	}
	if (Sphere)
	{
		//拾取武器后应设置碰撞预设为 无碰撞
		Sphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}
}

编辑动画 Editing Animations

慢动作命令 :slomo 0.1 0.1倍速运行游戏

打开动画序列 Standing_Melee_Attack_360_High

调整时间线到问题动画之前
image

选择要调整的骨骼 upperarm_r 右臂
image

点击 关键帧 以添加一个关键帧
image

这将添加 upperarm 曲线
image
如果我们双击它,我们会看到上臂及其变换的各个方面都在这里,例如平移和旋转,并且它被锁定到动画中的该点。
image

返回到主时间线 栏
image

将时间轴拖到问题动画之后的一段时间点上。
image
点击 关键帧 以添加一个关键帧。
现在这里有两个关键帧锁定这些位置。
现在,这还没有改变任何事情。
我想要的是改变上臂在与罐碰撞的地方的旋转。

将时间线移动到问题动画处,必须在之前2个关键帧之间。
image
此时可以调整相关的骨骼,upperarm_r。旋转 upperarm_r使其正常。

image
点击 关键帧 以添加一个关键帧。

image
我们移动的坐标发生变化。
双击曲线
image
移动的帧为折线。从开头正常帧回到最后一个关键帧。

回到主时间轴,预览动画,完成。

烘焙关键帧到动画

你可能想要一个没有任何关键帧的动画,在这种情况下,你可以烘焙这个为新的动画。

点击创建资产-创建动画-当前动画-动画数据
image
image
新动画将没有关键帧。

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

13.武器力学 Weapon Mechanics

碰撞盒 Collision Box

为武器添加碰撞盒组件 box。
为了击中物体,我们需要在我们的武器上有某种碰撞体积。盒子是碰撞体积的完美形状。

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

class UBoxComponent;

private:
	UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
	UBoxComponent* WeaponBox;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

#include "Components/BoxComponent.h"

AWeapon::AWeapon()
{
	WeaponBox = CreateDefaultSubobject<UBoxComponent>(TEXT("Weapon Box"));
	WeaponBox->SetupAttachment(GetRootComponent());
}

BP_Weapon

打开武器蓝图
Weapon Box(Weapon Box)(盒体碰撞)
通常用于简单碰撞的盒体。边界在编辑器中被渲染为线条。
源:继承的( C++ )
引入:Weapon
原生组件名称: Weapon Box
移动性:中可移动
仅编辑器: False

image

默认WeaponBox盒体碰撞为方形,与剑的形状不一致。
但是,不应该设置 WeaponBox盒体碰撞的变换-缩放。因为所有附加到WeaponBox上的物体也会收到缩放影响。
image
缩放应保持为1.

应当设置WeaponBox盒体碰撞的属性,即 盒体范围,
image

对于球体碰撞盒子,同样不设置缩放,而是设置半径。

调整WeaponBox的盒体范围,使其外形与剑一致
image

WeaponBox盒体碰撞 重叠事件

BP_Weapon 事件图

选中 WeaponBox 组件【选中才能添加特定事件】
为WeaponBox添加事件 on component begin overlap(WeaponBox)
image

image
仅用于调试。

image

追踪 Tracing

线条追踪 line
image

形状追踪 shape

image

盒子追踪 box

image

image

为武器添加盒子追踪

打开 BP_Weapon ,在剑头,剑末添加2个场景组件 Start,End

image
image

事件图
添加 盒体追踪【通道】box trace by channel

一旦物体与某物重叠则执行盒体追踪。与物体碰撞出绘制绿色盒子。
[碰撞双方都必须有碰撞体,启用重叠事件,否则盒体追踪查询无效]
half size 为框半径。
draw debug type 设置为 针对时长

image

在撞击点绘制调试球。

从 命中结果out hit 拖出 break hit resuit 提取数据节点
image
image

image

武器与其他角色的碰撞

将另2个 BP_LearnCharacter 拖入关卡。
默认情况下,角色 BP_LearnCharacter 的碰撞胶囊体具由重叠属性。剑会集中角色的胶囊体。
image
BP_LearnCharacter 的碰撞胶囊体组件碰撞预设为pawn.查询和物理。
对象类型为pawn.
image

BP_LearnCharacter 的 网格体Mesh组件碰撞预设为CharacterMesh.纯查询。
对象类型为pawn.
image

BP_Weapon 的 WeaponBox组件碰撞预设为 OverlapAllDynamic。纯查询。
对象类型为WorldDynamic.
image

使剑与角色网格交互

更改 BP_Weapon 的 WeaponBox组件碰撞预设为 custom.
忽略 pawn.
image
BP_LearnCharacter的胶囊体与网格体的碰撞对象类型均为pawn,不会与BP_Weapon发生重叠事件。

更改其他角色在关卡的实例的碰撞预设。
将被打击的一个 BP_LearnCharacter 实例的网格体组件的碰撞预设改为自定义。
对象类型改为 WorldDynamic.
勾选 生成重叠事件。
image
此时击打该角色可触发重叠事件,但不会触发击中box trace跟踪。
image

这是因为BP_Weapon的box trace跟踪只对visibility可视性通道跟踪。

image

如果其他对象设置为忽略visibility可视性,将无法对其进行box trace跟踪。
而 被击打的BP_LearnCharacter 实例的网格体组件的碰撞预设的检测响应-visibility默认为忽略。需将visibility改为阻挡。
image

现在 BP_Weapon的box trace跟踪对BP_LearnCharacter 实例的网格体生效。【打击点绘制球体】
image

盒体追踪 Box Trace in C++

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

protected:
	virtual void BeginPlay() override;

	//weapon box 重叠事件
	UFUNCTION()
	void OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);


private:
	UPROPERTY(VisibleAnywhere)
	USceneComponent* BoxTraceStart;

	UPROPERTY(VisibleAnywhere)
	USceneComponent* BoxTraceEnd;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

#include "Kismet/KismetSystemLibrary.h"

AWeapon::AWeapon()
{
	WeaponBox = CreateDefaultSubobject<UBoxComponent>(TEXT("Weapon Box"));
	WeaponBox->SetupAttachment(GetRootComponent());

	//设置碰撞预设 纯查询 
	WeaponBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	// 对所有通道可重叠
	WeaponBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Overlap);
	//单独设置对pawn对象类型忽略,忽略角色的胶囊体
	WeaponBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);

	BoxTraceStart = CreateDefaultSubobject<USceneComponent>(TEXT("Box Trace Start"));
	BoxTraceStart->SetupAttachment(GetRootComponent());

	BoxTraceEnd = CreateDefaultSubobject<USceneComponent>(TEXT("Box Trace End"));
	BoxTraceEnd->SetupAttachment(GetRootComponent());
}

void AWeapon::BeginPlay()
{
	Super::BeginPlay();

	WeaponBox->OnComponentBeginOverlap.AddDynamic(this, &AWeapon::OnBoxOverlap);
}

void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	const FVector Start = BoxTraceStart->GetComponentLocation();
	const FVector End = BoxTraceEnd->GetComponentLocation();

	TArray<AActor*> ActorsToIgnore;
	ActorsToIgnore.Add(this);
	FHitResult BoxHit;
	UKismetSystemLibrary::BoxTraceSingle(
		this,
		Start,
		End,
		FVector(5.f, 5.f, 5.f),
		BoxTraceStart->GetComponentRotation(),
		ETraceTypeQuery::TraceTypeQuery1,
		false,
		ActorsToIgnore,
		EDrawDebugTrace::ForDuration,
		BoxHit,
		true
	);
}

调整 BP_Weapon 蓝图 start,end 位置
image

设置被击打的BP_LearnCharacter 实例的网格体组件的碰撞预设的检测响应-visibility改为阻挡。
image

image

TArray 动态数组 Dynamic Arrays

TArray 可以自动扩容

禁用武器盒体碰撞 Disabling Weapon Box Collision

只在攻击时触发重叠事件.
控制 weapon box 组件的重叠事件何时发生.

可以在攻击动画中使用动画通知。
禁用碰撞,直到攻击时启用.攻击动画结束然后立即禁用该碰撞。

AM_AttackMontage

打开动画蒙太奇 AM_AttackMontage

添加轨道 EnableCollision 。

在武器击打开始时新建通知 EnableBoxCollision.

添加轨道 DIsableCollision。
在武器击打结束时新建通知 DisableBoxCollision.

image

C++

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

public:
	FORCEINLINE UBoxComponent* GetWeaponBox() const { return WeaponBox; }

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

AWeapon::AWeapon()
{
	WeaponBox = CreateDefaultSubobject<UBoxComponent>(TEXT("Weapon Box"));
	WeaponBox->SetupAttachment(GetRootComponent());

	//设置碰撞预设 纯查询 
	WeaponBox->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	// 对所有通道可重叠
	WeaponBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Overlap);
	//单独设置对pawn对象类型忽略,忽略角色的胶囊体
	WeaponBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);

	BoxTraceStart = CreateDefaultSubobject<USceneComponent>(TEXT("Box Trace Start"));
	BoxTraceStart->SetupAttachment(GetRootComponent());

	BoxTraceEnd = CreateDefaultSubobject<USceneComponent>(TEXT("Box Trace End"));
	BoxTraceEnd->SetupAttachment(GetRootComponent());
}

E:\Unreal Projects 532\Learn\Source\Learn\Public\Characters\LearnCharacter.h

public:
	UFUNCTION(BlueprintCallable)
	void EnableWeaponCollision();

	UFUNCTION(BlueprintCallable)
	void DisableWeaponCollision();

	UFUNCTION(BlueprintCallable)
	void SetWeaponCollisionEnabled(ECollisionEnabled::Type CollisionEnabled);

E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

#include "Components/BoxComponent.h"//碰撞盒子

void ALearnCharacter::SetWeaponCollisionEnabled(ECollisionEnabled::Type CollisionEnabled)
{
	if (EquippedWeapon && EquippedWeapon->GetWeaponBox())
	{
		EquippedWeapon->GetWeaponBox()->SetCollisionEnabled(CollisionEnabled);
		//EquippedWeapon->IgnoreActors.Empty();
	}
}

ABP_Echo

打开动画蓝图 ABP_Echo,事件图表。实现新的动画通知 EnableBoxCollision ,DisableBoxCollision。
事件中调用C++的SetWeaponCollisionEnabled,传入参数 查询,和空。
image

现在,只有攻击时才会触发盒体的碰撞。

虚幻接口 Unreal Interfaces

使用接口处理受击。

处理用剑击打物体以及处理受击响应。
武器上的盒子组件将触发重叠事件,然后我们进行盒子跟踪以查看我们击中某物的确切点。
这个hit result中包含了一个hit Actor。我们总是可以检查那个hit Actor并用它做一些事情。
应当在特定类中处理 hit Actor。
以适应击中不同物体。
创建一个名为hit接口的类,该类将有自己的功能。
但接口的特殊之处在于它们被设计为声明函数而不是实际定义。

这个接口可以被其他类继承。例如敌人,鹿等可被攻击的物体。
它的用途如此广泛,是因为我们的武器不需要知道它击中的是什么。
它需要做的就是查看 hit actor 是否实现了这个接口,然后就可以调用hit actor 的 get hit。
它可以简单地对它命中的任何东西调用 get hit 函数,并且每个类都将专门化它自己的行为。

鹿继承并实现了getHit接口,在接口中执行死亡。
武器击中鹿,获得hit actor ,将 hit actor 转换为 IHitInterface 类型,成功则执行 IHitInterface 的 getHit方法。
image

image

新建虚幻接口C++ HitInterface

image
image

E:\Unreal Projects 532\Learn\Source\Learn\Public\Interfaces\HitInterface.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "HitInterface.generated.h"

// This class does not need to be modified.
//此类不需要修改。
//该类仅参与虚幻反射系统
UINTERFACE(MinimalAPI)
class UHitInterface : public UInterface
{
	GENERATED_BODY()
};


// 这就是我们在利用多重继承时将要使用的接口
// 实际声明函数的类,由其他类继承
class LEARN_API IHitInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
	// 将接口函数添加到此类。这是将被继承以实现此接口的类。
public:
	// 可以在任何实现 GetHit 的类中重写它
	// = 0 表示纯虚函数,纯虚函数是不能在声明它的类中实现的函数。
	virtual void GetHit() = 0;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Interfaces\HitInterface.cpp

#include "Interfaces/HitInterface.h"

// Add default functionality here for any IHitInterface functions that are not pure virtual.
// 在此处为任何非纯虚拟的IHitInterface函数添加默认功能。
// 这里不会定义任何纯虚拟函数

敌人 Enemies

enemy character class
实现 HitInterface

新建敌人character类 Enemy
image
image
Enemy 具由网格体组件,可以被武器boxTrace击中。

因为 BP_Weapon 的Weapon Box 组件碰撞预设-碰撞响应-物体响应-pawn 为 忽略【其他为重叠】。
所以 Enemy 的 网格体组件 碰撞预设-对象类型 不能是 pawn类型。可以是 WorldDynamic 类型。

E:\Unreal Projects 532\Learn\Source\Learn\Public\Enemy\Enemy.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Enemy.generated.h"

UCLASS()
class LEARN_API AEnemy : public ACharacter
{
	GENERATED_BODY()

public:
	AEnemy();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

#include "Enemy/Enemy.h"
#include "Components/SkeletalMeshComponent.h"

AEnemy::AEnemy()
{
	PrimaryActorTick.bCanEverTick = true;

	//因为 BP_Weapon 的Weapon Box 组件碰撞预设-碰撞响应-物体响应-pawn 为 忽略【其他为重叠】。
	//所以  Enemy 的 网格体组件 碰撞预设 - 对象类型 不能是 pawn类型。可以是 WorldDynamic 类型。
	GetMesh()->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);

	//设置网格来阻挡可见通道,因为我们的武器box trace跟踪正在跟踪可见性通道,这样网格就能够被那个武器box trace击中
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
	// 忽略相机,防止镜头变焦忽大忽小
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GetMesh()->SetGenerateOverlapEvents(true);
}

void AEnemy::BeginPlay()
{
	Super::BeginPlay();
	
}

void AEnemy::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void AEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

基于 Enemy 新建蓝图 BP_Enemy

image

下载 敌人角色模型
https://www.mixamo.com/#/?page=1&type=Character
Character 栏:下载 圣骑士 paladin j nordstrom.
Paladin J Nordstrom.fbx

image
image

导入 Paladin J Nordstrom.fbx。
不选择 骨骼,因为 该角色自带骨骼。
image
image
整理材质
image

image

打开动画序列 Paladin_J_Nordstrom_Anim
,该动画序列只有一个 T-pose,没有实际动画。所以删除 动画序列 Paladin_J_Nordstrom_Anim。
image

骨骼网格体 命名为 SKM_Paladin。
物理资产 命名为 PA_Paladin
骨骼 命名为 SK_Paladin。
image

根运动动画 Root Motion Animations

动画会移动角色网格体的世界坐标。

下载圣骑士动画

https://www.mixamo.com/#/?page=1&query=idle+sword+and+shield&type=Motion%2CMotionPack
在 动画栏,搜索 idle sword and shield
image

下载动画 sword and shield idle。第一个动画包含皮肤。
image

根运动动画

打开 SKM_Paladin 骨骼网格体。【没有root根骨骼,不能使用虚幻 根运动动画】
SKM_Paladin 的骨骼层级最顶层为臀骨 Hips.位于身体中间,是实际骨骼。

image

而 角色 Echo 的骨骼层级最顶部为 root 根骨骼。位于角色脚底部平面。用于 根运动动画。

image
root 根骨骼连接并控制臀骨pelvis。
image

在虚幻中,根运动动画只能用于带root根骨骼的网格体。
mixamo 动画 没有root根骨骼,
所以不能使用圣骑士的会导致角色位移的动画。
需要为 mixamo 动画 添加root根骨骼。

下载另一个位移运动动画

只要至少有一个带皮肤的动画,其余的动画都可以不带皮肤,以节省空间。
下载动画 standing react large from back,standing react large from front,standing react large from right,standing react large from left。都不选皮肤。
image

将带有皮肤的动画 Sword And Shield Idle.fbx 重命名为 idle with skin.fbx

在blender 中为圣骑士角色动画添加根骨骼root转为根动画。使用插件 mixamo_converter

下载 mixamo_converter 为压缩包

mixamo_converter是为添加根骨骼到骨骼网格物体或具有骨骼网格物体的动画而设计的。
它是专门为我们可以使用 Maximo 的根部运动动画而设计的。

安装插件 mixamo_converter
打开 blender-编辑-偏好设置-插件-安装-选择压缩包即可

image

启用插件
image

删除所有元素。
右侧打开 mixamo 工具栏。
image

convert single 可以导入并转换单个动画。

batch convert 可以导入并转换多个动画。

点击 input 选择 没有根动画的文件夹,

点击输出选择根动画输出文件夹。

展开高级选项,取消勾选应用旋转 apply rotation.
取消 tansfer totation.[置灰]

image

点击 batch convert 批量转换。batch convert 变灰时完成。

删除 不带root根骨骼的 PA_Paladin,SK_Paladin,SKM_Paladin。
只保留材质文件夹。
image

首先导入 带根骨骼,带网格体皮肤的动画 idle with skin.fbx 。
不选择骨骼。自带骨骼。
image

默认没有关联材质。
image

打开 骨骼网格体 idle_with_skin。
已经包含了 root根骨骼,位于底部地平面。root连接控制臀骨Hips.
此时骨骼网格体可以使用根运动动画。
image

切换到 资产详情 选项卡,分配材质 M_Paladin

image
image

重命名 骨骼网格体 idle_with_skin 为 SKM_Paladin
image

删除默认白色材质

重命名 物理资产 为 PA_Paladin
重命名 动画序列 idle_with_skin_Anim 为 idle
重命名 骨骼 为 SK_Paladin。

现在,它还不是一个根运动动画,但重点是我们的角色本身必须有一个根骨骼。因为动画是特定于骨骼的。

导入剩余动画,选择骨骼 SK_Paladin.
取消勾选 导入网格体。因为我们没有要导入的网格。我们只要动画。
image
image

双击它们,我可以看到它们实际上正在移动角色。这些是根运动动画。
image
如果我们没有根骨骼,我们无法使用动画本身来移动我们的角色。但现在这些可以。
重命名为 ReactFromBack,ReactFromFront,ReactFromLeft,ReactFromRight.
image

为敌人分配网格体资产

打开 BP_Enemy-网格体组件-网格体-骨骼网格体资产-选择骨骼网格体 SKM_Paladin
image

使用 网格体组件:动画资产 预览
image
image

调整 骨骼网格体
image

拖入关卡 BP_Enemy
image
武器可以命中敌人
image

继承Hit接口 Implementing Interfaces

使敌人对受击做出响应。
enemy 继承 HitInterface。

E:\Unreal Projects 532\Learn\Source\Learn\Public\Interfaces\HitInterface.h

public:
	// 可以在任何实现 GetHit 的类中重写它
	// = 0 表示纯虚函数,纯虚函数是不能在声明它的类中实现的函数。
	// const 引用避免 ImpactPoint 被复制,修改
	virtual void GetHit(const FVector& ImpactPoint) = 0;

E:\Unreal Projects 532\Learn\Source\Learn\Public\Enemy\Enemy.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Interfaces/HitInterface.h"//继承时必须在头文件包含
#include "Enemy.generated.h"

//继承 IHitInterface 接口
UCLASS()
class LEARN_API AEnemy : public ACharacter, public IHitInterface
{
	GENERATED_BODY()

public:
	AEnemy();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	// 覆盖 IHitInterface 的 GetHit
	virtual void GetHit(const FVector& ImpactPoint) override;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

#include "Learn/DebugMacros.h"

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint, FColor::Orange);
}

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

#include "Interfaces/HitInterface.h"

void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	const FVector Start = BoxTraceStart->GetComponentLocation();
	const FVector End = BoxTraceEnd->GetComponentLocation();

	TArray<AActor*> ActorsToIgnore;
	ActorsToIgnore.Add(this);
	FHitResult BoxHit;
	UKismetSystemLibrary::BoxTraceSingle(
		this,
		Start,
		End,
		FVector(5.f, 5.f, 5.f),
		BoxTraceStart->GetComponentRotation(),
		ETraceTypeQuery::TraceTypeQuery1,
		false,
		ActorsToIgnore,
		EDrawDebugTrace::ForDuration,
		BoxHit,
		true
	);

	//如果击中了物体
	if (BoxHit.GetActor())
	{
		IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor());
		if (HitInterface)
		{
			//如果转换为 IHitInterface,则执行他的 GetHit
			HitInterface->GetHit(BoxHit.ImpactPoint);
		}
	}
}

在敌人受击点绘制球体。
image

受击动画蒙太奇 Hit React Montage

新建 AM_HitReact 动画蒙太奇-选择 骨骼 SK_Paladin
image

打开 AM_HitReact 动画蒙太奇,为 DefaultGroup.Default 蒙太奇轨道,从右侧资产浏览器选择添加4个受击动画序列
image
image

在个动画序列起始处添加对应蒙太奇片段名称 FromFront,FromBack,FromLeft,FromRight.
删除默认片段名,
image
右侧 蒙太奇片段-清除
image

制作受击动画蓝图 ABP_Paladin

骨架-选择 骨骼 SK_Paladin
image
image

image
现在我们的 Paladin 应该播放它的空闲动画,但是如果我们播放使用默认插槽的蒙太奇,该蒙太奇将接管,我们将看到该蒙太奇部分在我们的角色上发挥作用。

为敌人设置动画

BP_Enemy-网格体组件-动画-动画模式-使用动画蓝图
BP_Enemy-网格体组件-动画-动画类-ABP_Paladin
image

播放受击响应蒙太奇动画 Playing the Hit React Montage

E:\Unreal Projects 532\Learn\Source\Learn\Public\Enemy\Enemy.h

class UAnimMontage;//蒙太奇动画

private:

	UPROPERTY(EditDefaultsOnly, Category = "Montages")
	UAnimMontage* HitReactMontage; // 攻击蒙太奇动画

protected:
	void PlayHitReactMontage(const FName& SectionName);//播放蒙太奇动画

E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint, FColor::Orange);
	PlayHitReactMontage(FName("FromLeft"));
}

void AEnemy::PlayHitReactMontage(const FName& SectionName)
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && HitReactMontage) {
		AnimInstance->Montage_Play(HitReactMontage);
		AnimInstance->Montage_JumpToSection(SectionName, HitReactMontage);
	}
}

为BP_Enemy 蓝图设置动画蒙太奇

打开 BP_Enemy-Montages-hit react montage-AM_HitReact
image

此时攻击敌人,敌人受击动画播放完后会被拉回到原位置。
此时没有使用到根运动动画。

启用根运动动画

打开 动画序列 ReactFromLeft -资产详情-根运动-启用根运动 勾选
image
image

点积 Dot Product

返回标量
angle between wo vectors 通过点积和反余弦两个矢量之间的角度
image

E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

#include "Kismet/KismetSystemLibrary.h"

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint, FColor::Orange);
	PlayHitReactMontage(FName("FromLeft"));

	const FVector Forward = GetActorForwardVector();//归一化的朝向向量

	//使击中点下降至敌人中线点位置,保持水平计算
	const FVector ImpactLowered(ImpactPoint.X, ImpactPoint.Y, GetActorLocation().Z);
	//击中点 减去 敌人中心点,然后归一化
	const FVector ToHit = (ImpactPoint - GetActorLocation()).GetSafeNormal();

	//点积
	//Forward * ToHit =|Forward| |ToHit| * cos(theta)
	//|Forward| =1 ,|ToHit| = 1,所以 Forward * ToHit = cos(theta) 
	const double CosTheta = FVector::DotProduct(Forward, ToHit);

	//反余弦 即夹角 弧度制
	const double Theta = FMath::Acos(CosTheta);

	//radians弧度转角度degrees
	const double deg = FMath::RadiansToDegrees(Theta);

	//从敌人中心画出箭头
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + Forward * 60.f, 5.f, FColor::Red, 5.f);

	//从敌人中心到命中位置箭头
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ToHit * 60.f, 5.f, FColor::Green, 5.f);

}

image

交叉乘积 Cross Product

返回向量
计算角度的正负。
image

E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint, FColor::Orange);
	PlayHitReactMontage(FName("FromLeft"));

	const FVector Forward = GetActorForwardVector();//归一化的朝向向量

	//使击中点下降至敌人中线点位置,保持水平计算
	const FVector ImpactLowered(ImpactPoint.X, ImpactPoint.Y, GetActorLocation().Z);
	//击中点 减去 敌人中心点,然后归一化
	const FVector ToHit = (ImpactPoint - GetActorLocation()).GetSafeNormal();

	//点积
	//Forward * ToHit =|Forward| |ToHit| * cos(theta)
	//|Forward| =1 ,|ToHit| = 1,所以 Forward * ToHit = cos(theta) 
	const double CosTheta = FVector::DotProduct(Forward, ToHit);

	//反余弦 即夹角 弧度制
	const double Theta = FMath::Acos(CosTheta);

	//radians弧度转角度degrees
	double deg = FMath::RadiansToDegrees(Theta);

	//计算叉积
	// 如果这个叉积朝上,表示右边被击中,两个命中向量位于前方的右侧。
	//但如果它指向下方,那么我们就会从左侧受到打击,就像击中的是前方的左侧一样。
	const FVector CrossProduct = FVector::CrossProduct(Forward, ToHit);

	if (CrossProduct.Z < 0)
	{
		deg *= -1.f;
	}
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + CrossProduct * 100.f, 5.f, FColor::Blue, 5.f);

	//从敌人中心画出箭头
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + Forward * 60.f, 5.f, FColor::Red, 5.f);

	//从敌人中心到命中位置箭头
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ToHit * 60.f, 5.f, FColor::Green, 5.f);

}

定向打击反应 Directional Hit Reactions

基于打击角度选择动画蒙太奇

image
E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint, FColor::Orange);

	const FVector Forward = GetActorForwardVector();//归一化的朝向向量

	//使击中点下降至敌人中线点位置,保持水平计算
	const FVector ImpactLowered(ImpactPoint.X, ImpactPoint.Y, GetActorLocation().Z);
	//击中点 减去 敌人中心点,然后归一化
	const FVector ToHit = (ImpactPoint - GetActorLocation()).GetSafeNormal();

	//点积
	//Forward * ToHit =|Forward| |ToHit| * cos(theta)
	//|Forward| =1 ,|ToHit| = 1,所以 Forward * ToHit = cos(theta) 
	const double CosTheta = FVector::DotProduct(Forward, ToHit);

	//反余弦 即夹角 弧度制
	double Theta = FMath::Acos(CosTheta);

	//radians弧度转角度degrees
	Theta = FMath::RadiansToDegrees(Theta);

	//计算叉积
	// 如果这个叉积朝上,表示右边被击中,两个命中向量位于前方的右侧。
	//但如果它指向下方,那么我们就会从左侧受到打击,就像击中的是前方的左侧一样。
	const FVector CrossProduct = FVector::CrossProduct(Forward, ToHit);

	if (CrossProduct.Z < 0)
	{
		Theta *= -1.f;
	}

	FName Section("FromBack");

	if (Theta >= -45.f && Theta < 45.f)
	{
		Section = FName("FromFront");
	}
	else if (Theta >= -135.f && Theta < -45.f)
	{
		Section = FName("FromLeft");
	}
	else if (Theta >= 45.f && Theta <= 135.f)
	{
		Section = FName("FromRight");
	}

	PlayHitReactMontage(FName(Section));

	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + CrossProduct * 100.f, 5.f, FColor::Blue, 5.f);

	//从敌人中心画出箭头
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + Forward * 60.f, 5.f, FColor::Red, 5.f);

	//从敌人中心到命中位置箭头
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ToHit * 60.f, 5.f, FColor::Green, 5.f);

}

每次挥剑一次 One Hit Per Swing

忽略击中的actor

E:\Unreal Projects 532\Learn\Source\Learn\Public\Enemy\Enemy.h

public:
	void DirectionalHitReact(const FVector& ImpactPoint);

E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint, FColor::Orange);

	DirectionalHitReact(ImpactPoint);

}

void AEnemy::DirectionalHitReact(const FVector& ImpactPoint)
{
	const FVector Forward = GetActorForwardVector();//归一化的朝向向量

	//使击中点下降至敌人中线点位置,保持水平计算
	const FVector ImpactLowered(ImpactPoint.X, ImpactPoint.Y, GetActorLocation().Z);
	//击中点 减去 敌人中心点,然后归一化
	const FVector ToHit = (ImpactPoint - GetActorLocation()).GetSafeNormal();

	//点积
	//Forward * ToHit =|Forward| |ToHit| * cos(theta)
	//|Forward| =1 ,|ToHit| = 1,所以 Forward * ToHit = cos(theta) 
	const double CosTheta = FVector::DotProduct(Forward, ToHit);

	//反余弦 即夹角 弧度制
	double Theta = FMath::Acos(CosTheta);

	//radians弧度转角度degrees
	Theta = FMath::RadiansToDegrees(Theta);

	//计算叉积
	// 如果这个叉积朝上,表示右边被击中,两个命中向量位于前方的右侧。
	//但如果它指向下方,那么我们就会从左侧受到打击,就像击中的是前方的左侧一样。
	const FVector CrossProduct = FVector::CrossProduct(Forward, ToHit);

	if (CrossProduct.Z < 0)
	{
		Theta *= -1.f;
	}

	FName Section("FromBack");

	if (Theta >= -45.f && Theta < 45.f)
	{
		Section = FName("FromFront");
	}
	else if (Theta >= -135.f && Theta < -45.f)
	{
		Section = FName("FromLeft");
	}
	else if (Theta >= 45.f && Theta <= 135.f)
	{
		Section = FName("FromRight");
	}

	PlayHitReactMontage(FName(Section));

	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + CrossProduct * 100.f, 5.f, FColor::Blue, 5.f);

	//从敌人中心画出箭头
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + Forward * 60.f, 5.f, FColor::Red, 5.f);

	//从敌人中心到命中位置箭头
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ToHit * 60.f, 5.f, FColor::Green, 5.f);
}

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

public:
	TArray<AActor*> IgnoreActors;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	const FVector Start = BoxTraceStart->GetComponentLocation();
	const FVector End = BoxTraceEnd->GetComponentLocation();

	TArray<AActor*> ActorsToIgnore;
	ActorsToIgnore.Add(this);

	for (AActor* Actor : IgnoreActors)
	{
		ActorsToIgnore.AddUnique(Actor);
	}

	FHitResult BoxHit;
	UKismetSystemLibrary::BoxTraceSingle(
		this,
		Start,
		End,
		FVector(5.f, 5.f, 5.f),
		BoxTraceStart->GetComponentRotation(),
		ETraceTypeQuery::TraceTypeQuery1,
		false,
		ActorsToIgnore,
		EDrawDebugTrace::ForDuration,
		BoxHit,
		true
	);

在攻击蓝图AM_AttackMongtage中的禁用碰撞DisableBoxCollision通知时,清空攻击忽略的actor数组。
E:\Unreal Projects 532\Learn\Source\Learn\Private\Characters\LearnCharacter.cpp

void ALearnCharacter::SetWeaponCollisionEnabled(ECollisionEnabled::Type CollisionEnabled)
{
	if (EquippedWeapon && EquippedWeapon->GetWeaponBox())
	{
		EquippedWeapon->GetWeaponBox()->SetCollisionEnabled(CollisionEnabled);
		//在攻击蓝图AM_AttackMongtage中的禁用碰撞DisableBoxCollision通知时,清空攻击忽略的actor数组。
		EquippedWeapon->IgnoreActors.Empty();
	}
}

打击音效 Hit Sounds

新建 MetaSound源 sfx_HitFlesh
image
image

E:\Unreal Projects 532\Learn\Source\Learn\Public\Enemy\Enemy.h

private:
	UPROPERTY(EditDefaultsOnly, Category = "Sounds")
	USoundBase* HitSound;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

#include "Kismet/GameplayStatics.h"

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint, FColor::Orange);

	DirectionalHitReact(ImpactPoint);

	if (HitSound) {
		UGameplayStatics::PlaySoundAtLocation(
			this,
			HitSound,
			ImpactPoint
		);
	}
}

为 BP_Enemy 设置声效

BP_Enemy-Sounds -HitSound- sfx_HitFlesh
image

音效衰减 资产

随听众距离衰减
右键-音频-音效衰减
SA_HitFlesh
image
image

打开 SA_HitFlesh
image

衰减函数:它被设置为线性等数学函数来确定声音的行为
衰减形状:默认设置为球形,但您也可以更改该形状。基本上这意味着我们将有一个球体,一个围绕声源的不可见球体。
内部半径:这个球体将有一个半径。这个内半径属性决定了我们在听到最大音量的声音之前可以接近多远。因此默认情况下它设置为 400,这意味着如果我们距离声源 400 单位或更少,我们将听到音源的全部音量。
衰减距离:距离之外将听不到声音。

为 sfx_HitFlesh 使用 SA_HitFlesh

打开 sfx_HitFlesh
image

sfx_HitFlesh-源 标签-衰减-衰减设置-SA_HitFlesh
image

命中粒子 Hit Particles 【视觉效果,简称 VFX】

受击后生成流血粒子。
虚幻引擎有两个不同的系统来创建视觉效果。
它有cascade,也有niagara。
cascade是遗留系统.
image

cascade

E:\Unreal Projects 532\Learn\Source\Learn\Public\Enemy\Enemy.h

private:
	UPROPERTY(EditDefaultsOnly, Category = "VisualEffects")
	UParticleSystem* HitParticles;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	//DRAW_SPHERE_COLOR(ImpactPoint, FColor::Orange);

	DirectionalHitReact(ImpactPoint);

	if (HitSound) {
		UGameplayStatics::PlaySoundAtLocation(
			this,
			HitSound,
			ImpactPoint
		);
	}
	if (HitParticles)
	{
		UGameplayStatics::SpawnEmitterAtLocation(
			this,
			HitParticles,
			ImpactPoint
		);
	}
}

为 BP_Enemy 设置HitParticles
打开 BP_Enemy -VisualEffects-HitParticles
image

武器轨迹 Weapon Trails

下资 paragon:Minions 资产
虚幻争霸:小兵
Epic Games - Epic内容 - 2018/03/09
只允许在基于虚幻引擎的产品中使用。包含《虚幻争霸》的小兵和丛林野生生物的角色模型、动画和皮肤。
image
image

调整攻击速度/增加 Trails 轨道

打开 动画蒙太奇 AM_AttackMontage
选中一个片段
image
动画片段-播放速率-2
image
当我这样做时,我注意到我对蒙太奇部分的攻击被向前推进了。
将不得不重新定位所有动画通知。

新建Trails 轨道用于武器追踪

右键-添加通知状态-尾部
image
trails 尾部通知状态有一个持续时间。
image
选中 trails -细节面板-动画通知-PS模板-搜索 trail
image

首个插槽名称 和 第二个插槽名称 定义尾迹的位置。
打开 Echo_Skeleton 骨骼

在 hand_r 添加插槽 FirstTrailSocket
image
调整 FirstTrailSocket 到剑刃下部
image
在 hand_r 添加插槽 SecondTrailSocket
调整 SecondTrailSocket 到剑尖
image

打开 动画蒙太奇 AM_AttackMontage
选中 Trail通知状态-动画通知
首个插槽名称-FirstTrailSocket
第二个插槽名称-SecondTrailSocket
image
image

复制一份 Trail通知状态 到动画片段2
image

image

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

可破坏 Breakable Actors

可破坏网格 Destructible Meshes

using chaos physics to break pots
用混沌物理学破坏罐子

进入 破裂模式/几合体集

image

下载一个陶瓷罐子模型pot
image
首先要创建一个文件夹Destructibles来保存我们想要的项目的所有几合体集。

几合体集基本上是我们在断裂网格后获得的一组静态网格
选择罐子后-点击 新建 指定文件夹Destructibles-创建几合体集
image
image
image
image
image

fracture :有不同的断裂方法,使用不同的算法随机地将这个网格分解成小块。
image

选择 罐子 -点击 -fracture-簇
image

选择 罐子 -点击 -fracture-统一
image

点击 破裂 应用该效果
image
image
image
选择部分0,可以多次点击 破裂 应用效果。
image

它们的子部分取决于所受伤害的程度。
这些可破坏的网格物体基于内部损坏系统,并且造成的损坏基于取决于多种因素。
例如,如果对该网格体施加一定的力,那么如果它超过了一定的损坏阈值,网格会破裂。

进入选项模式-选择罐子,将其举高,启用重力。播放后会掉落摔碎。
image

显示原始材质

-混沌物理-general-显示骨骼颜色 取消勾选
image

##物理场系统actors / Field System Actors
using fields to break destructibles
使用物理场破坏可破坏网格
https://docs.unrealengine.com/5.3/zh-CN/overview-of-physics-fields-in-unreal-engine/

新建 物理场系统actors
右键-蓝图类- Field System Actor
BP_FieldSystem
image

这只是一个actor,包含物理场系统组件.
场系统组件是能够产生影响物理场的组件。
image

RadialFalloff 径向衰减

添加 RadialFalloff 径向衰减 组件
image

打开 BP_FieldSystem 事件图表,添加外力。
image

set radial falloff:作为外部张力的属性。
field magnitude 与 可破坏的网格-伤害阈值 相关
sphere radius 球体半径将决定物体应该距离多近才能受到力。

对物理场进行了径向衰减,这将产生一种压力。
image

可破坏的网格物体具有相当高的损坏阈值

打开 可破坏的网格 -伤害阈值
image

image

通过外力破碎

将 BP_FieldSystem 拖入 可破坏的网格 旁边,将使其破碎。
image
image
如果我们将其移到该半径之外,我们将不会获得那么多的力。

RadialVector 径向向量

使这些碎片都朝一个方向飞
为 BP_FieldSystem 添加一个 RadialVector 径向向量组件。
对碎片施加 RadialVector 方向的线性力
image

FieldSystemMetaDataFilter 场系统过滤

为 BP_FieldSystem 添加一个FieldSystemMetaDataFilter 场系统过滤组件。
默认场系统会对角色的布料产生影响,需过滤角色。
image
选择 SystemMetaDataFilter 场系统过滤组件-细节面板-场-object类型-破坏
这将使场系统只对可破坏网格产生力。
image
image
将 SystemMetaDataFilter 属性赋予线性力 的 meta data 节点
image

破碎后堆在一起

选择 可破坏物体-细节面板-clustering-启用创建簇
启用后,碎片将聚集。需要更大的力场使其破碎。
不启用,碎片将分散。

image

为武器创造物理场 Creating Fields with Weapons

使用武器破坏可破坏网格

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Weapons\Weapon.h

protected:
	//BlueprintImplementableEvent 表示蓝图可实现函数,,C++定义,蓝图实现
	UFUNCTION(BlueprintImplementableEvent)
	void CreateFields(const FVector& FieldLocation);

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	const FVector Start = BoxTraceStart->GetComponentLocation();
	const FVector End = BoxTraceEnd->GetComponentLocation();

	TArray<AActor*> ActorsToIgnore;
	ActorsToIgnore.Add(this);

	for (AActor* Actor : IgnoreActors)
	{
		ActorsToIgnore.AddUnique(Actor);
	}

	FHitResult BoxHit;
	UKismetSystemLibrary::BoxTraceSingle(
		this,
		Start,
		End,
		FVector(5.f, 5.f, 5.f),
		BoxTraceStart->GetComponentRotation(),
		ETraceTypeQuery::TraceTypeQuery1,
		false,
		ActorsToIgnore,
		EDrawDebugTrace::ForDuration,
		BoxHit,
		true
	);

	//如果击中了物体
	if (BoxHit.GetActor())
	{
		IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor());
		if (HitInterface)
		{
			//如果转换为 IHitInterface,则执行他的 GetHit
			HitInterface->GetHit(BoxHit.ImpactPoint);
		}
		IgnoreActors.AddUnique(BoxHit.GetActor());
		// 击中物体时,在击打点产生物理场使其受力可破碎
		CreateFields(BoxHit.ImpactPoint);
	}
}

打开 BP_Weapon 实现 CreateFields

事件图-添加事件 CreateFields
image

为 BP_Weapon 添加组件 FieldSystem场系统组件,RadialFalloff 径向衰减,FieldSystemMetaDataFilter 场系统过滤,RadialVector 径向向量
image

FieldSystemMetaDataFilter -细节面板-场-object类型-破坏
image

image

设置 罐子 可破坏物体-碰撞- 生成重叠事件
image

打开 罐子 可破坏物体 蓝图
image
细节面板-碰撞-尺寸特定数据-索引0-碰撞形态-索引0-隐式类型-胶囊体。【更容易发生碰撞】
定义如何初始化刚体碰撞结构的CollisionType。
image

Breakable Actor

新建C++ Actor 类 BreakableActor
image
为了使用 UGeometryCollectionComponent ,需要添加模块 GeometryCollectionEngine
E:\Unreal Projects 532\Learn\Source\Learn\Public\Breakable\BreakableActor.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "BreakableActor.generated.h"

class UGeometryCollectionComponent;

UCLASS()
class LEARN_API ABreakableActor : public AActor
{
	GENERATED_BODY()
	
public:	
	ABreakableActor();

protected:
	virtual void BeginPlay() override;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	UGeometryCollectionComponent* GeometryCollection;
public:	
	virtual void Tick(float DeltaTime) override;

};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Breakable\BreakableActor.cpp

#include "Breakable/BreakableActor.h"
#include "GeometryCollection/GeometryCollectionComponent.h"



ABreakableActor::ABreakableActor()
{
	PrimaryActorTick.bCanEverTick = false;

	GeometryCollection = CreateDefaultSubobject<UGeometryCollectionComponent>(TEXT("GeometryCollection"));
	SetRootComponent(GeometryCollection);
	GeometryCollection->SetGenerateOverlapEvents(true);
       //防止碎片引起相机缩放
	GeometryCollection->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GeometryCollection->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);

}

void ABreakableActor::BeginPlay()
{
	Super::BeginPlay();
	
}

void ABreakableActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

基于 BreakableActor 新建蓝图类 BP_BreakableActor
image

打开 BP_BreakableActor-GeometryCollection组件-细节面板-混沌物理-其他集-选择一个可破坏物体
image
image
将 BP_BreakableActor 放入关卡,可击打破碎

BreakableActor 实现 HitInterface 接口

E:\Unreal Projects 532\Learn\Source\Learn\Public\Breakable\BreakableActor.h

#include "Interfaces/HitInterface.h"

UCLASS()
class LEARN_API ABreakableActor : public AActor, ,public IHitInterface

public:	
	virtual void GetHit(const FVector& ImpactPoint, AActor* Hitter) override;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Breakable\BreakableActor.cpp

void ABreakableActor::GetHit(const FVector& ImpactPoint, AActor* Hitter)
{
}

蓝图原生事件 Blueprint Native Event

1.c++中定义事件,c++和蓝图中都可以实现(c++必须实现)。

2.执行优先级:

如果蓝图不实现,会执行c++函数实现。
如果蓝图和c++都实现,蓝图会覆盖c++实现从而只执行蓝图实现。

定义 GetHit 蓝图原生事件

E:\Unreal Projects 532\Learn\Source\Learn\Public\Interfaces\HitInterface.h

public:
	// BlueprintNativeEvent  蓝图原生事件,不可以是虚函数 virtual,也不可以是纯虚函数 =0
	//现在幕后发生的事情是虚幻引擎将创建一个 C++ 特定版本虚函数
	UFUNCTION(BlueprintNativeEvent)
	void GetHit(const FVector& ImpactPoint);

继承覆盖实现 GetHit_Implementation

E:\Unreal Projects 532\Learn\Source\Learn\Public\Enemy\Enemy.h

class LEARN_API AEnemy : public ACharacter, public IHitInterface

public:
	// 覆盖实现 IHitInterface 的 蓝图原生事件 GetHit
	virtual void GetHit_Implementation(const FVector& ImpactPoint) override;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Enemy\Enemy.cpp

void AEnemy::GetHit_Implementation(const FVector& ImpactPoint)

E:\Unreal Projects 532\Learn\Source\Learn\Public\Breakable\BreakableActor.h

class LEARN_API ABreakableActor : public AActor, public IHitInterface

public:	
	// 覆盖实现 IHitInterface 的 蓝图原生事件 GetHit
	virtual void GetHit_Implementation(const FVector& ImpactPoint) override;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Breakable\BreakableActor.cpp

void ABreakableActor::GetHit_Implementation(const FVector& ImpactPoint)
{
}

调用 GetHit_Implementation

现在需要虚幻引擎的反射系统来以稍微不同的方式处理这个接口 GetHit。
当我们从 C++ 调用它们时,我们实际上使用 Execute_GetHit 这个自动生成的执行函数来执行它们

HitInterface->Execute_GetHit(BoxHit.GetActor(), BoxHit.ImpactPoint);

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp

void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
//HitInterface->GetHit(BoxHit.ImpactPoint);
//如果转换为 IHitInterface,则执行他的 GetHit
//当我们从 C++ 调用它们时,我们实际上使用这个自动生成的执行函数来执行它们
//新增 Actor 参数
HitInterface->Execute_GetHit(BoxHit.GetActor(), BoxHit.ImpactPoint);
}

蓝图中实现 GetHit

BP_BreakableActor 重命名为 BP_Breakable
打开 BP_Breakable-事件图表-添加事件 GetHit
image
齿轮图标表示这是从接口继承的函数。
并且当在 C++ 中调用 GetHit 时,该事件就会被触发。
如果需要调用C++的 GetHit_Implementation 实现【默认执行蓝图版本,则不执行C++版本】,需要 蓝图手动调用父版本【C++版本】的GetHit。
在 GetHit 节点右键-将调用添加到父函数 【添加一个调用此函数父项的节点】
image

image

现在 击打圣骑士 使用C++版本 GetHit。Enemy。
击打罐子,为空函数。BP_Breakable。

Breaking Sounds

为打碎罐子添加音效
新建 MetaSound源 sfx_PotBreak
image
image

BP_Breakable

打开 BP_Breakable 播放音效
image

为可破坏物体设置寿命使碎片消失

set life span 设置3秒寿命
image

只在 chaos 粉碎时间时 设置寿命

选择 BP_Breakable-GeometryCollection组件-右键-添加事件-添加 on chaos break event
选择 BP_Breakable-GeometryCollection组件-混沌物理-events- 通知中断.[如为true,该组件将生成其他系统可能订阅的破坏事件]
image
image

image

from blog.

WangShuXian6 avatar WangShuXian6 commented on June 6, 2024

宝藏 Treasure

宝藏 Treasure

从可破坏物体生成拾取品
下载 资源包 ancient Treasure
image

新建拾取音效 MetaSound源 sfx_Treasure
image
image

基于 Item C++类 新建 Treasure C++类

image
image

E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Treasure.h

#pragma once

#include "CoreMinimal.h"
#include "Items/Item.h"
#include "Treasure.generated.h"

UCLASS()
class LEARN_API ATreasure : public AItem
{
	GENERATED_BODY()

protected:
	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;

private:
	UPROPERTY(EditAnywhere, Category = Sounds)
	USoundBase* PickupSound;
};

E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Treasure.cpp

#include "Items/Treasure.h"
#include "Characters/LearnCharacter.h"
#include "Kismet/GameplayStatics.h"

void ATreasure::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	ALearnCharacter* LearnCharacter = Cast<ALearnCharacter>(OtherActor);
	if (LearnCharacter)
	{
		if (PickupSound)
		{
			UGameplayStatics::PlaySoundAtLocation(
				this,
				PickupSound,
				GetActorLocation()
			);
		}
		Destroy();
	}
}

新建 BP_Treasure 蓝图类 ,继承 Treasure类

选择组件 Item Mesh ,细节面板-静态网格体-静态网格体-SM_Chalice
image
选择根 BP_Treasure,细节面板-sounds-pickup sound-sfx_Treasure
image
可以添加声音衰减。

将 BP_Treasure 放入关卡
image

Spawning Actors

在打破罐子后生成宝藏

BP_Treasure 网格体组件碰撞预设设置无碰撞

防止与所在罐子碰撞。

BP_Treasure-ItemMesh-碰撞预设-custom
BP_Treasure-ItemMesh-碰撞预设-碰撞已启用-NoCollision 无碰撞
image

image

BP_Breakable 蓝图中生成 BP_Treasure

打开 BP_Breakable
添加节点 spawn actor from class
添加节点 get actor transform
添加节点 make transform
添加节点 add
分割结构体引脚
image

注意:当前的第二个击打动作偶尔会引起BP_Breakable 的 GetGit无限脚本递归。尚未解决。

在 C++中生成宝藏 Spawning Actors from C++

UClass 表示该类是一个指针,可以指向一个蓝图类或C++类,后期在蓝图编辑器中指定带网格的具体蓝图 BP_Treasure

image

E:\Unreal Projects 532\Learn\Source\Learn\Public\Breakable\BreakableActor.h

private:
	// UClass 表示该类是一个指针,可以指向一个蓝图类或C++类,后期在蓝图编辑器中指定带网格的具体蓝图 BP_Treasure
	UPROPERTY(EditAnywhere)
	UClass* TreasureClass;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Breakable\BreakableActor.cpp

#include "Items/Treasure.h"

void ABreakableActor::GetHit_Implementation(const FVector& ImpactPoint)
{
	UWorld* World = GetWorld();
	if (World && TreasureClass)
	{
		FVector Location = GetActorLocation();
		Location.Z += 75.f;

		World->SpawnActor<ATreasure>(TreasureClass, Location, GetActorRotation());
	}
}

在BP_Breakable蓝图中指定 TreasureClass 带网格的 BP_Treasure
image

注意:纯 C++版本没有无限递归问题

TSubclassOf

使用宝藏泛型,可指定多种宝藏。
TSubclassOf 包装器是一个结构体,通常是一个模板类型,旨在包装指针。它为我们存储该指针。
它们通常是模板,因此通常以 t 开头。
E:\Unreal Projects 532\Learn\Source\Learn\Public\Breakable\BreakableActor.h

private:
	//ATreasure 类型的宝藏,限制只能派生自 ATreasure C++类或以下,防止蓝图中选择了错误的,非宝藏类型的类
	UPROPERTY(EditAnywhere)
	TSubclassOf<class ATreasure> TreasureClass;

为可破坏添加胶囊体组件,防止角色穿过。

E:\Unreal Projects 532\Learn\Source\Learn\Public\Breakable\BreakableActor.h

protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	class UCapsuleComponent* Capsule;

E:\Unreal Projects 532\Learn\Source\Learn\Private\Breakable\BreakableActor.cpp

#include "Components/CapsuleComponent.h"

ABreakableActor::ABreakableActor()
{
	PrimaryActorTick.bCanEverTick = false;

	GeometryCollection = CreateDefaultSubobject<UGeometryCollectionComponent>(TEXT("GeometryCollection"));
	SetRootComponent(GeometryCollection);
	GeometryCollection->SetGenerateOverlapEvents(true);
	GeometryCollection->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	//使角色可以穿过可破坏物体和它的碎片
	GeometryCollection->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);

	//使用胶囊体阻止角色穿过可破坏物体,之后物体销毁后只留下碎片
	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetupAttachment(GetRootComponent());
	Capsule->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
	Capsule->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Block);
}

调整 BP_Breakable 胶囊体大小

image
image
调整形状/半径,而非缩放。
image

设置胶囊体 pawn 通道的碰撞响应

添加节点 set collision response to channel
在胶囊体的破碎事件中设置胶囊体碰撞响应为忽略 pawn。允许角色穿过遗留的胶囊体。
image

from blog.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.