Open WangShuXian6 opened 10 months ago
在静态网格体编辑界面,按住ALT,使用右键拖动,可以缩放网格体视角 按住ALT,使用左键拖动,可以围绕网格体旋转视角 按住右键拖动,围绕世界旋转视角
在场景中,按住shift同时移动物体,可使相机跟随移动 G键,然后F11,进入沉浸浏览
新建 空白开放世界 关卡 默认包含: 世界数据层 WorldDataLayers 世界分区小地图 WorldPartitionMiniMap
开启 世界分区 面板 世界分区 显示当前位置
加载世界分区指定部分 左键选择范围,右键-从选择加载区域
sky atmosphere 天空大气旨在创建类似地球的大气,可以散射光线,就像真实的大气一样
定向光模拟无限远的光源。这意味着投射在世界上所有物体上的阴影都是彼此平行的。 使用定向光源来模拟太阳。
标量scalar/数字量:距离0点的值
具有起点和终点的箭头称为矢量/向量。 向量包含两个信息:方向和大小。 向量具有明确定义的方向,因为我们知道它的起始位置和结束位置。 向量也有一个明确定义的大小,因为我们知道它有多长。 2D 矢量具有 X 和 Y 分量 向量仅由其方向和大小定义
两个相同的向量
向量减法公式用于计算两个向量相减的结果。给定两个向量 $( \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) ]
向量减法在物理、工程学和数学中都有广泛应用,例如在计算物体的位移时。
向量的幅度(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 \) 分别是向量在各个坐标轴上的分量。向量的幅度总是非负的,代表着向量的“大小”。在物理学和工程学中,了解向量的幅度非常重要,因为它表示了例如速度、力等物理量的大小。
向量归一化(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。向量归一化在计算机图形学、物理学、机器学习等领域非常重要,尤其是在需要处理方向而不是长度的场景中。
虚幻引擎使用左手坐标系
在虚幻引擎(Unreal Engine)中,Pitch、Yaw 和 Roll 是用来描述对象在三维空间中的旋转的三个术语,它们分别对应于不同的旋转轴:
俯仰定义为给定对象绕 y 轴的旋转。 俯仰角:Pitch 是围绕y 轴的旋转,它决定了对象的上下倾斜程度。想象一下,当你点头或者抬头时,你的头部就是在做 Pitch 运动。
旋转定义为物体绕 z 轴的旋转 Yaw(偏航角):Yaw 是围绕 z 轴的旋转,它决定了对象的左右转向。比如,当你左右转头时,你的头部就是在做 Yaw 运动。
Roll(翻滚角):Roll 是围绕 x 轴的旋转,它影响对象的倾斜。想象一下,当你倾斜头部,使耳朵接近肩膀时,这就是 Roll 运动。
vs菜单栏-右键-自定义 命令-工具栏-标准 解决方案配置 解决方案平台 启动项目
修改所选内容
反射是程序在运行时检查自身的能力 程序将分析其自身内部发生的情况并收集有关该程序的数据。 C++没有内置反射,但虚幻引擎有自己设计的反射系统来收获这些数据 该反射系统负责将数据与Unreal Ed系统合并将其中一些数据暴露给蓝图并使用垃圾自动处理内存分配
【item蓝图继承自item C++类时】item蓝图先运行,然后item C++类运行
将头文件放入public文件夹,cpp放入私有文件夹
新建文件后,等待 live coding 完成,再点击VS的 全部重新加载 item Actor类 的位置 这将显示该类的父类,所属模块,路径
双击改类将在代码编辑中打开文件
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++ 没有显示对象,拖入世界后将 不显示网格体之类的元素 通常不会直接放置原始 Actor C++ 因此可以基于Actor C++ 新建蓝图Actor
Actor 蓝图则是 Actor C++ 的子类,继承 Actor C++ 的所有功能 蓝图提供用户友好的方式与类互动
在内容区 新建蓝图类 BP_Item
将 BP_Item 拖入关卡
双击 BP_Item 进入蓝图编辑器
Actor 有自己的本地坐标 Actor 必须有一个根组件,默认为 DefaultSceneRoot 场景根组件
顶部标签组
事件图是我们可以放置蓝图节点来执行蓝图逻辑的地方 同C++
添加打印 需要 取消勾选
情景关联
以使用开发功能更新蓝图后需要 编译并保存
播放时界面显示调试信息 hello
print string 更像是一个 C++ 函数,而 Begin play 是一个 C++ 事件
将鼠标悬停在开始播放事件上的这个小方块,它会显示输出委托,表示这是委托事件
它在游戏开始前被调用 只要该蓝图上的属性发生更改,就会执行它 例如,我们可以进入蓝图的详细信息面板并更改其中的变量属性,每次我们更改某些内容时,该构造脚本都会启动 所以这对于在游戏开始之前做事情很有好处
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代码]
在开放世界地图中,如果摄像机远离指定Actor,即跨越相关的地图分区,该actor则与玩家不在相关,该actor执行的调试信息将不再显示。
新建普通地图进行测试
按D键可复制蓝图
Key 允许我们指定一个值来确定这些打印字符串消息的行为 如果我们不设置它,较新的消息显示在顶部,并且将两条消息打印到屏幕上
如果这两个打印字符串都使用相同的key,则新消息将替换旧消息 而不是像现在这样堆积起来
如果我将第一个打印字符串的键设置为 1,并将第二个打印字符串的键也设置为 1,
然后当我点击播放时,我只会看到第二个,因为第一个被第二个取代
Delta秒是一个浮点值,为我们提供自上次以来已经过去的时间量
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"));
}
}
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](https://github.com/WangShuXian6/blog/assets/30850497/11e4a94d-6892-4b0c-b4ab-daa2ce3b7e5f)
VS中选中 FString - 速览定义 可查看定义
![image](https://github.com/WangShuXian6/blog/assets/30850497/1c45b824-049c-4846-9816-badc77617f04)
#### 绘制调试球体 Drawing Debug Spheres
##### 蓝图中绘制
获取自身位置-绘制球体
![image](https://github.com/WangShuXian6/blog/assets/30850497/daf5c54c-ee72-444f-ae70-d9ddc677ebd5)
![image](https://github.com/WangShuXian6/blog/assets/30850497/4e4736b4-a520-45ad-a57e-6404e1b75f77)
##### C++ 中绘制
需要包含头文件 绘制调试助手
`PCGdemo\Source\PCGdemo\Private\Items\Item.cpp`
```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);
}
}
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);
}
draw debug line
需要起点和终点坐标
起点用当前actor坐标表示
get actor forward vector
将获取对象的方向向量,但长度为1,
当前actor坐标 + 方向向量,即朝该方向移动1cm,终点坐标相对actor的指定方向偏移1cm,这样画出来的调试线过短。
故需要将get actor forward vector
乘以100,成为长度100的向量,这样 终点坐标相对actor的指定方向偏移100cm,显示较好
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);
}
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);
}
调试点大小相对视图保持不变,无论距离相机多远,始终可以被看到。
如果宏自带分号结尾,在调用宏的时候依然可以增加分号结尾。
宏内代码换行时,每行结尾添加反斜杠\
,但最后一行代码不需要添加反斜杠
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);
}
在source内的项目名称根目录处右键-添加-新建项-显示所有模板-头文件-命名为 DebugMacros
更改文件目录为项目根目录 \Learn\Source\Learn
,与public同级
点击添加即可
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项目
编辑器内删除的文件,并未在本地磁盘中实际删除,需要手动删除
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);
}
actor绕Z轴旋转90度
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);
}
蓝图子类的运行早于父类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);
}
正弦函数是一个在数学中非常重要的三角函数,通常用符号
sin
表示。它定义为直角三角形中对边与斜边的比值,也可以在单位圆上定义。正弦函数有两种常见的角度表示方法:度数(角度)和弧度。
角度(Degrees):是一种测量角大小的单位,其中一个完整圆被划分为 360 度。在三角函数中,当提到角度时,我们通常是指这个度量。
弧度(Radians):是一种更自然的方式来衡量角度,尤其在数学和物理学中。一个完整的圆等于 (2\pi) 弧度。弧度基于圆的半径,一个弧度等于半径长度的圆弧。
正弦函数的值随角度的变化而变化,无论是以度数还是弧度表示。在不同的应用中,可能会根据需要使用度数或弧度。例如,在物理学中,弧度是更常用的单位,而在一些工程和导航应用中,则可能更多地使用度数。
正弦函数在周期为 2π 弧度或 360 度的区间内变化,呈现周期性波动。
蓝图 左侧可以添加变量
变量节点可以直接拖入编辑器的时间图表中 可以将 变量 T 拖出并释放,然后我们就可以获取它或设置它 按住 alt 键单击可以切断连线
将每帧的 T 值设置为其旧值加上增量时间 因此,每一帧我们都会将 Delta 时间添加到称为 T 的运行时间变量中
虚幻引擎有两种版本的正弦函数,一种采用弧度radians,另一种采用角度degrees
选择正弦弧度 为sin函数传递时间,正弦是一个返回值的函数,随着输入值的增加,它会平滑地上下波动
调用函数 ad Actor World 偏移量,改变每一帧的 Z 值,
右键-delta location-分割结构体引脚-以单独更改Z 正弦会增加和减少,增加和减少直到正数,一遍又一遍地回到负值,但方式很平稳。
控制波动幅度
初始化变量的方式: 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++的变量 可以自定义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; //振幅
实例的值会覆盖蓝图的值
UPROPERTY(VisibleDefaultsOnly)
// 蓝图中可见,但不可编辑
// 实例中不可见
UPROPERTY(VisibleDefaultsOnly)
float RunningTime;
UPROPERTY(VisibleInstanceOnly)
// 实例中可见,但不可编辑
// 蓝图中不可见
UPROPERTY(VisibleInstanceOnly)
float RunningTime;
UPROPERTY(VisibleAnywhere)
// 实例和蓝图中都可见,但不可编辑
UPROPERTY(VisibleAnywhere)
float RunningTime;
这对于在游戏运行时更改这些变量的值非常有用【事件中更改】
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; //振幅
必须是公开类型 public 或保护类型 protected ,且 BlueprintReadWrite ,BlueprintReadOnly 才可以被继承
clamp 将振幅骑限制在 0-1 之间 振幅会越来越小到静止
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sine Parameters")
float Amplitude = 0.25f; //振幅
事件图可访问,但只读
private: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) float TimeConstant = 5.f;//正弦上升所需时间
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);
}
模板函数的实现必须写在类型定义文件中
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);
}
actor可以拥有组件,这是为actor提供功能的一种方式。
假设我们有一个名为 Weapon 的Actor。 需要一个网格,因此我们将为此提供一个组件。 就可以看到我们的武器在世界上的样子。
当您挥动武器时,您可能希望知道武器何时击中物体。 可以在武器网格的刀片周围放置一个不可见的碰撞盒子组件,当我们击中敌人时可以用它来检测重叠事件。
每个Actor至少有一个组件,一旦我们创建了蓝图项目,我们就在组件中看到它有一个默认的场景根组件。
但在C++中,它被称为RootComponent,其类型是 USceneComponent。 场景组件的功能非常有限,具有 Transform 功能。 包括 Location,Rotation,Scale。 场景组件能够附加到其他组件。
每当我们调用 get actor location 并获取代表Actor位置的 f 向量时。 这个函数实际上返回根组件的位置。 换句话说,它返回存储在根组件中的变换变量Transform。 这就是为什么一个 actor 必须至少有一个组件,即包含该变换的根组件。
由于场景组件支持附件,我们可以向Actor添加一个新的场景组件将其附加到根。 通过附加到根,这意味着场景组件将随着根组件,始终保持它们的相对距离恒定。 因此,如果根组件在世界中移动,场景组件也会随之移动。
场景组件派生的其他一些类: 静态网格体组件 组件面板中的静态网格物体名称并不意味着我们刚刚添加了一个新的静态网格物体。 相反,我们添加了一个新的静态网格体组件对象。
静态网格体组件有自己的变换,并且可以附加到其他组件。 静态网格物体组件类有一个Static Mesh 变量。 静态网格物体组件有一个静态网格物体变量,所以我们需要指定使用什么网格物体
静态网格物体的枢轴点位于网格的底部,而不是网格的中心 所以在建模软件中,枢轴点也要在底部
按 E 进入旋转模式并旋转它
静态网格体组件源自场景组件。 所以它们实际上本身就是场景组件,这意味着我们可以重新分配静态网格物体组件成为默认场景根。 只需单击静态网格并将其拖动到默认场景根,默认根目录将被删除 现在我们的根是静态网格物体组件,这将无法在视口中移动该网格物体,因为这现在是蓝图的参考框架。 它永远不会相对于自身移动
当我们在虚幻引擎中创建一个类时,比如我们的Item类,它派生于actor类, 虚幻引擎会根据该类创建一个类默认对象或 CDO。 类默认对象是虚幻引擎反射系统中包含的对象的主副本。 它是在引擎启动时创建的。 编译我们的代码本质上也开始了它的生成。 它保存反射系统可以访问的默认值来初始化属性。
基于该类,在世界中创建的蓝图对象。 当引擎初始化时,它为每个类创建这些类默认对象。 然后它执行每个类的构造函数,设置它们的默认值。 蓝图获取由引擎初始化的默认值,并且该信息来自类和默认对象。 所有这一切都发生在我们的幕后,所以我们可以制作游戏,甚至不知道这个过程正在发生。
了解这个过程很重要,因为给定类的构造函数在游戏引擎进程的早期执行,通常在游戏构造函数中做某些事情为时过早。 如果我们想在游戏开始时做某事并确保游戏中的所有对象都已初始化,我们必须在begin play 开始播放时而不是在对象的构造函数中进行初始化
假设我们有Item类,并且添加了一个名为Item mesh的组件。 要添加这个组件,我们必须为此组件创建一个默认的子对象。 默认子对象的行为很像类默认对象,只是它用于组件本身. 它包含该子对象的所有默认属性. 虚幻引擎组件是其所属 actor 的子对象.
创建默认子对象时,必须提供一些信息 需要指定子对象的对象类型. 例如,使用静态网格组件,我们还需要提供一个内部名称. 该名称与实际变量名称本身不同,由虚幻引擎使用. 出于各种目的,主要是跟踪不同的对象以创建默认的子对象。
子对象可以采用不同的组件类型. 因此,在尖括号中我们提供了组件类型,例如使用静态网格组件,并且在括号中我们提供对象的名称,这就是内部名称. 将字符串文字包装在文本宏中.确保它们采用 Unicode 格式. 这就是我们为新组件创建默认子对象的方式
创建默认子对象返回新创建的子对象的地址,以便我们可以存储它到一个指针中
为我们创建对象的函数称为工厂函数。 在 C++ 中,您可能习惯使用 new 关键字来构造对象的新实例,并且将该对象的地址存储在指针中。 在虚幻引擎中,我们很少使用 new 关键字,因为引擎具有这些工厂函数为我们构造对象并执行大量内部工作,例如注册该对象用以让引擎知道它
要在 C++ 中创建一个新组件,我们必须创建一个默认的子对象。
E:\Unreal Projects 532\Learn\Source\Learn\Public\Items\Item.h
private: // 静态网格体组件类型的指针 // 只是一个空指针 // UPROPERTY() 反射负责垃圾回收 指针为空时删除对象 UPROPERTY(VisibleAnywhere) UStaticMeshComponent* ItemMesh;
在构造函数中,将创建一个新的静态网格物体组件
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蓝图效果如下
因为静态网格体组件的静态网格变量尚未设置,所以当前视口中看不到任何东西。 最好的做法是在 C++ 中创建静态网格体组件并在蓝图设置其静态网格体属性,使得它更加通用。 因为我们可以创建多个Item蓝图并单独设置它们的静态网格属性。
在编辑器中,虚幻引擎为拆分C++定义的名称使其更加可读。 蓝图中的 Item Mesh 对应 C++ 中的 ItemMesh 变量
蓝图中 ItemMeshComponent 对应 C++ 中CreateDefaultSubobject 文本宏包裹的 ItemMeshComponent 通常不会让场景组件在任何地方进行编辑,而是让它们在任何地方都可见。 场景组件本身在详细信息面板中有自己的属性,可以更改这些属性。 包含位置、旋转和缩放的完整变换。 如果这个特定组件是根组件,我们就无法更改它的位置或其轮换
为其指定静态网格体
要更改其位置,我们只能在世界中actor的实例上执行此操作.
当前 BP_Item 在x,y轴做正弦晃动
更改蓝图使其在z轴上下正弦晃动
继承自 actor 类,但可以被控制器所拥有。
工具-新建C++类-选择 Pawn 类作为父类,创建新的C++类。
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);
}
虚幻引擎-虚幻商城-ANIMAL VARIETY PACK
该资源-Rigged: Yes ,这意味着它们可以被动画化 点击-免费 点击-添加到工程 可以将 AnimalVarietyPack 目录复制到工程 Content 目录下
由于 鸟的静态网格体三角形太多,检测碰撞需要大量计算,非常昂贵,
所以使用胶囊体组件 Capsule Component 进行碰撞检测。一个具有一些基本几何形状的组件。
它的几何形状非常简单,并且还能够在编辑器中渲染
在
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 蓝图[纯数据蓝图]-打开完整蓝图编辑器
在组件面板中,可以看到胶囊体组件 可以在那里放置一个人物角色
细节面板 修改胶囊体大小:
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);
}
现在可以使用胶囊进行碰撞检测出,而不必使用我们实际的鸟网格体。
直接将
#include "Components/CapsuleComponent.h"
包含在Bird.h
头文件中不是最佳实践。
使用前置声明代替直接包含 b.h
有几个潜在的好处:
减少编译依赖:如果 a.h
只是使用了 b.h
中声明的一部分内容(如某些类或函数的引用),前置声明可以减少文件之间的编译依赖。这样做有助于加快编译速度,因为更改 b.h
不会直接导致需要重新编译包含 a.h
的所有文件。
避免循环依赖:在某些情况下,使用前置声明可以帮助避免头文件之间的循环依赖问题。
减少编译时间:虽然包含头文件通常不会显著增加单个文件的大小,但它可能增加编译时间,尤其是在大型项目中,头文件被多次包含时。
避免隐式包含: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 文件还会包含其他文件,
预处理器会找到所有这些层层包含的文件代码,粘贴到 Bird.h
文件中,
而实际编译器不需要知道所有细节,只需要知道用到的 UCapsuleComponent 的信息。
在 头文件中,只是创建了 UCapsuleComponent 类型的一个指针。
在 cpp中,才会实际创建 UCapsuleComponent 对象,此时,需要直到 UCapsuleComponent 的详细信息,在内存分配多少空间。
但是需要先在头文件定义 UCapsuleComponent 组件才能在 cpp中构造该类型组件,所以,可以在头文件中声明一个 UCapsuleComponent 名称的变量
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;
};
静态网格物体无法进行动画处理, 骨骼网格体组件可以制作动画。
就像静态网格物体组件有自己的静态网格物体变量一样. 骨骼网格体组件类有它自己的变量,称为骨骼网格物体 USkeletalMesh. 并且骨架网格物体有骨架 Skeleton.因为它有骨架,所以可以制作动画.
SK_Crow
骨骼网格体 资产面板包含了 材质,骨骼。 骨骼网格体 骨骼树 具由骨骼层次结构
显示骨骼: 角色-骨骼-所有层级 骨架中的骨骼已按权重绘制到网格上,当这些骨骼之一移动时,网格物体将随之扭曲并移动。 可以单击其中一些骨骼并移动它们。。 因为网格是权重绘制的,因此网格多边形上的顶点会跟随骨头。
将骨架连接到网格体并将网格体权重绘制到骨骼上称为绑定。 rig为网格体创建骨架,并使这些网格体能够与骨骼一起变形和动画。
动画是包含与骨骼运动相关的数据的资产.网格可以设置动画. 包含这些骨骼的运动信息 该动画仅与该特定网格相关联. 右上可以切换 :骨骼网格体预览,骨骼网格体编辑器,骨骼动画。
骨骼网格体预览 可以查看共享骨骼的其他 骨骼网格体。
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 蓝图 Capsule 胶囊组件 下有了 BirdMesh 骨骼网格体组件, 骨骼网格体组件-细节面板 -可以设置 骨骼网格体资产 和 动画。
设置一个 骨骼网格体资产 SK_Crow
现在它默认面向 Y 方向, 虚幻引擎actor应当面向x方向,红色箭头。 左下角为参照系
旋转 BirdMesh 骨骼网格体组件90度使其朝前。
向下移动 BirdMesh 骨骼网格体组件 使其底部与根组件胶囊体底部对齐
将 BP_Bird 拖入关卡创建实例 默认无法控制鸟的移动
BirdMesh 骨骼网格体组件-细节面板-动画-动画模式-使用动画资产
选择动画序列[底部绿色线段] BirdMesh 骨骼网格体组件-细节面板-动画-要播放的动画-选择一个动画序列
此时视口将播放动画序列
在引擎中,只要您开始玩游戏,就会生成一个控制器并将其分配给玩家。 这个控制器是你在游戏中的代表。 游戏看到控制器并认为那是您, 控制器允许我们选择要占有的 Pawn 例如 BP_Bird
所有的 pawn 都有一个名为“自动拥有玩家 auto possess player”的变量,我们可以设置它的值. 它是未设置的,但如果我们将其设置为玩家0,则玩家0指的是世界上的第一个控制器. 控制器.从零开始计数.
如果世界上有多个控制器,例如在多人分屏游戏中,那么第一个控制器将是玩家零号,第二个控制器将是玩家一号. 在我们的例子中,只有一个控制器,这将是零号玩家.
通过设置我们的 pawn的 auto possess player变量为玩家零,只有我们游戏中的控制器才会拥有小鸟,然后我们就可以控制小鸟并让它飞.
虚幻引擎会在游戏开始时默认生成一个pawn供玩家控制【如果没有指定pawn】 点击该按钮即可和玩家控制器分离,查看引擎生成的默认pawn.[因为当前没有配置pawn]
自动控制玩家
默认为禁用
该控制器指代当前电脑上的控制器,非多人网络游戏控制器。 当前电脑上的控制器应选玩家0,目前只有一个玩家。 此时按下播放将无法移动视角。因为控制器已经占有了BP_Bird实例对象,鸟。 按 shift+F1 即可看到,此时没有默认生成的灰色圆球。
自动控制玩家
AutoPossessPlayerE:\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;
}
编辑-项目设置-引擎-输入-操作映射,轴映射
设置轴映射 wsad scale 为负值时表示反向
准备轴映射绑定函数
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);
}
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 细节面板可设置速度等属性
此时播放按W可以移动。
添加向后绑定S
完成前后绑定。
选中胶囊组件,将相机添加到胶囊组件的子级,和胶囊组件一起移动。 将相机组件向后移动 可在关卡看到鸟
向上移动相机,然后向下倾斜相机,形成自上而下的视图。 这不是最佳实践,不应将相机直接连接到根组件,而应将相机连接到弹簧臂组件
将相机组件移到弹簧臂组件子级 此时相机的变换将错误,设置相机组件-细节面板-变换-位置 旋转 全部归零 弹簧臂组件 的变换也需要归零 红线表示弹簧臂组件 弹簧臂组件会根据相机与物体得到重叠自动收缩,可设置臂长 旋转 弹簧臂
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;
}
此时蓝图已拥有弹簧臂和相机组件 旋转弹簧臂组件
如果实例的自动控制玩家无法设置,可在蓝图设置,蓝图会覆盖C++. BP_Bird-细节面板-pawn-自动控制玩家
为控制器添加旋转 FRotation
为鼠标创建轴映射,控制鸟的旋转
AController 控制器 拥有旋转,没有位置,缩放,因为不可见
轴映射也在每一帧运行
绑定函数。
蓝图 BP_Bird -细节面板-Pawn-使用控制器旋转Yaw-勾选【默认未勾选】 此时鼠标可以旋转鸟的方向
控制台命令:show collision 显示碰撞体 目前胶囊体与控制器一起移动
俯仰角。 就是抬头或低头,和大地水平面的夹角。 绑定函数。
绑定函数。
蓝图 BP_Bird -细节面板-Pawn-使用控制器旋转Yaw-勾选【默认未勾选】
此时俯仰角控制与预期相反,所以设置 轴映射-LookUp-鼠标Y-Scale 为 -1
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-Capsule-细节面板-碰撞-碰撞预设 默认没有碰撞 碰撞预设-改为 BlockAll 可以碰撞 为关卡添加一个网格-碰撞预设-改为 BlockAll,用来测试碰撞。
游戏默认使用项目设置的GameModeBase,其中包含默认pawn,即代表玩家的可控制的灰色圆球。
基于 Game Mode Base 新建 BP_BirdGameMode 打开 BP_BirdGameMode 蓝图 细节面板-类-默认pawn类- 选择 BP_Bird。 因为蓝图版本的鸟具有网格体,而C++版本的 Bird 没有网格体。
主界面-世界场景设置-游戏模式-游戏模式重载-选择 BP_BirdGameMode 此时关卡将不在生成默认灰色圆球pawn,而直接使用鸟。
使用默认游戏模式 Game Mode,在开放地图跨越多个地图区域时,会导致pawn丢失,pawn被留在之前的世界分区,未激活时卸载。导致BP_Bird也被卸载。
将 玩家出生点 拖入关卡即可
https://docs.unrealengine.com/5.3/zh-CN/characters-in-unreal-engine/ 添加 CharacterMovementComponent、CapsuleComponent 和 SkeletalMeshComponent 后,
Pawn类可延展为功能完善的 角色 类。 角色用于代表垂直站立的玩家,可以在场景中行走、跑动、跳跃、飞行和游泳。 此类也包含基础网络连接和输入模型的实现。 骨架网格体组件 与pawn不同的是,角色自带 SkeletalMeshComponent,可启用使用骨架的高级动画。可以将其他骨架网格体添加到角色派生的类,但这才是与角色相关的主骨架网格体。 如需了解骨架网格体的更多内容,请参见:
骨架网格体 骨架网格体动画系统 胶囊体组件 CapsuleComponent 用于运动碰撞。为了计算 CharacterMovementComponent 的复杂几何体,会假设角色类中的碰撞组件是垂直向的胶囊体。如需了解碰撞的更多信息,请参见:
胶囊体组件 角色移动组件 CharacterMovementComponent 能够使人身不使用刚体物理即可行走、跑动、飞行、坠落和游泳。 其为角色特定,无法被任何其他类实现。 可在 CharacterMovementComponent 中设置的属性包含了摔倒和行走摩擦力的值、在空气、水、土地中行进的速度、浮力、重力标度,以及角色能对物理对象施加的物理力。 CharacterMovementComponent 还包含来自动画的根运动参数, 其已转换到世界空间中,可直接用于物理。
从虚幻引擎-示例-导入 古代山谷 项目 Echo 人物资产
将古代山谷 的 \Content\AncientContent\Characters
目录迁移到 使用项目的 Content\
目录
内容侧滑菜单-all-C++类-learn-Public-右键-新建C++类-LearnCharacter
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);
}
为角色添加输入控制
角色蓝图有一个蓝色箭头-指示角色的前进方向,指向x轴 胶囊体为角色根组件,由C++创建,所以无法从继承C++的蓝图中删除,重命名。 新的场景组件将自动附加到它,附加了所有其他组件,当然,角色运动组件除外。
角色移动组件 CharacterMovementComponent
角色移动组件不需要附加到任何东西,因为它没有网格,它没有视觉表示,有点像控制器,它是不可见的。 它是为角色设计。 默认情况下,它具有重力,最大加速度、制动摩擦系数。
胶囊体组件
箭头组件
骨架网格体组件 网格体-细节面板-网格体-骨架网格体资产-选择 Echo 骨架网格体 旋转 Echo 骨架网格体 使其面向x轴,向下移动使其底部与胶囊体底部对齐
网格体-细节面板-动画-动画类-选择 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, 打开 BP_BirdGameMode。 更改 类-默认pawn类-BP_LearnCharacter
当前关卡-世界场景设置-游戏模式-游戏模式重载-BP_BirdGameMode
项目设置 编辑-项目设置-项目-地图和模式-默认模式-默认游戏模式-BP_BirdGameMode 此时可以控制角色 BP_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;
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 调整摄像机位置角度 播放游戏 角色默认启用重力,将会从边缘跌落。
选中 BP_LearnCharacter-细节面板-pawn-使用控制器旋转yaw-取消勾选
同时,可以在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;
}
我们不希望角色继承旋转偏航俯仰,但角色相机与控制器一起移动. 选中 相机弹簧臂,弹簧臂的旋转继承自根组件即胶囊体组件, 但我们不希望胶囊体组件与控制器一起旋转。
选中 相机弹簧臂-细节面板-摄像机设置-继承yaw-勾选 选中 相机弹簧臂-细节面板-摄像机设置-使用pawn控制旋转-勾选 此时可以自由控制摄像机弹簧臂。弹簧臂与控制器一起旋转,而角色保持静止。
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);
}
}
获得控制器前向向量和右向向量
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](https://github.com/WangShuXian6/blog/assets/30850497/df54c448-6715-4007-85c0-073fd4709208)
![image](https://github.com/WangShuXian6/blog/assets/30850497/d930cb6f-0dfb-47fb-93d5-00eb20f8a6cb)
### 修改角色旋转速率
BP_LearnCharacter蓝图-选择 角色移动组件-细节面板-角色移动(旋转设置)-旋转速率-z-
### 在c++中设置 角色移动组件 的 旋转朝向,旋转速率
```cpp
// 角色移动组件
#include "GameFramework/CharacterMovementComponent.h"
ALearnCharacter::ALearnCharacter()
{
//角色移动组件 将旋转朝向运动
GetCharacterMovement()->bOrientRotationToMovement = true;
//角色移动组件 旋转速率
GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);
}
定位角色的网格体资源位置 BP_LearnCharacter蓝图-左侧 选择 网格体组件-细节面板-啊王个体-骨骼网格体资产-点击搜索图表
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/
同时也需要包含 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项目
启用插件 配置模块后,必须先启用相关插件,然后编写相关代码,否则报错。
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蓝图,可以看到头发,眉毛组件
分别为 头发,眉毛组件指定 -groom-groom asset
打开 BP_LearnCharacter蓝图-hair 组件-细节面板-材质-元素0-点击搜索,定位材质实例
MI_EchoGroom-右键复制一份-为 MI_EchoGroom_Color 打开 MI_EchoGroom_Color 材质实例 更改 color-tipColor 的颜色值 更改 Pepper-pepper color 的颜色值
BP_LearnCharacter蓝图-hair 组件-细节面板-材质-元素0-选择新的材质实例 MI_EchoGroom_Color
新建动画蓝图 ABP_Echo 内容侧滑菜单-右键-动画-动画蓝图 选择骨骼-
打开 动画蓝图 ABP_Echo
资产浏览器 可选择动画 添加到动画图
选中拖入的动画节点
细节面板-设置-循环动画-控制是否循环播放动画
BP_LearnCharacter蓝图-选中网格体组件-细节面板-动画-动画模式-使用蓝图动画 细节面板-动画-动画类-ABP_Echo 此时人物可以奔跑
打开 ABP_Echo 动画蓝图。
左侧面板新建变量 Character ,类型为 BP_LearnCharacter,类型选择 对象引用。
软引用表示延迟加载,指向项目的本地文件路径。 对象引用本质上就像 C++ 对对象的引用,而类引用就像保存类型的变量。
这类似创建了一个空指针。 需要初始化该变量。 该动画蓝图可以获取使用该动画蓝图的骨骼网格体组件的Pawn. 因为 BP_LearnCharacter 的 骨骼网格体组件中动画的设置使用了该蓝图, 运行游戏,则存在使用该动画蓝图的实际BP_LearnCharacter对象。
每次动画更新/每一帧都会调用事件 event blueprint update animation
在游戏开始时或拥有的 pawn 生成时调用一次 event blueprint initizlize animation
使用 event blueprint initizlize animation
初始化事务,例如 设置动画蓝图的 变量 Character。
try get pawn owner
返回一个当前角色BP_LearnCharacter的 pawn 类型的 对象引用.
但 变量 Character 为 BP_LearnCharacter 类型 对象引用.不可以直接设置。
使用cast,父类可以转换为子类,然后设置 变量 Character。
角色需要通过移动组件获取角色的运动状态,例如移动速度,是否在空中。
变量-角色-get Character movement
将 Character movement 提升为变量
重命名为 MovementComponent
现在根据该变量制作动画,
event blueprint update animation
中更改动画。从 MovementComponent 获取速度向量 get velocity
从 速度向量 获取向量大小长度 vector lengthXY
将 vector lengthXY 提升为变量 GroundSpeed 即角色的xy轴地面移动速度
每一帧动画都更新 GroundSpeed 变量
根据 GroundSpeed 播放不同动画。
在动画图中添加状态机
重命名为 Ground Locomotion 状态机 输出节点输出动画姿势数据,用来驱动角色动画。
添加状态节点 命名为 Idle [空闲] 为 Idle [空闲] 连接一个 Run 运动状态 状态之间的箭头表示转换规则 从 Run 向 Idle 拖出一个过渡
双击 Idle 进入Idle 状态,Idle 状态具由动画姿势输出节点,输出给 Ground Locomotion 状态机。
从右侧 资产浏览器 为 Idle 状态 拖入一个 空闲动画
双击Run 进入Run 状态 从右侧 资产浏览器 为 Run 状态 拖入一个 跑步动画
目前包含2个规则: 从Idle到Run,从Run到Idle.
双击顶部规则,设置 何时从Idle到Run。。 规则界面包含一个结果节点,输出参数为bool类型。 如果为true,则执行该规则,由空闲过渡到跑步。
GroundSpeed 大于0时返回true,过渡到跑步。
双击底部规则,设置 何时从Run 到 Idle。 GroundSpeed 等于0时返回true,过渡到空闲。
ABP_Echo 动画蓝图右上显示了该蓝图的父类,[一个C++类] 动画蓝图的 类设置 选项可以设置父类
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 变量和相关节点,只保留如下节点。
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 动画蓝图-时间图表 删除所有节点
类设置-类选项-父类 修复状态机中的变量错误,删除节点中旧的GroundSpeed,重新使用 移动分组下的GroundSpeed变量为父类的GroundSpeed 。 只保留 移动分组下的GroundSpeed
创建操作映射
轴映射每一帧都会被执行 操作映射只在触发是执行一次,例如按下空格。 操作映射比轴映射更有效。
如果每一帧都会运行,那么轴映射就是正确的选择。 操作映射非常适合一次性动作,例如跳跃、攻击、捡起物品、扔石头。
设置 Jump 操作映射
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);
}
在 父类 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, 主状态将负责角色转变为不同的状态,例如,在空中.
添加 OnGround 状态
OnGround 状态 使用 Ground Locomotion 状态机缓存的姿势数据
缓存 Ground Locomotion 状态机的姿势变量为 Ground Locomotion,使其可在别处访问
在 主状态机 Main States 的 OnGround 状态 中使用 缓存的 Ground Locomotion
主状态机 Main States 添加 InAir 在空中 状态
进入 InAir 状态,添加 Jump_Idle_Fall 动画,并取消循环动画
设置 OnGround 状态 过渡到 InAir 状态 的规则 使用 IsFalling 的值。
在主状态机 Main States中,直接将 着陆动画 Jump_Idle_Land 拖入界面,即可新增着陆状态 Land
设置 InAir 状态 过渡到 着陆Land 状态 的规则 IsFalling 取反
着陆动画完成后过渡到 OnGround 状态
选中 着陆动画完成后过渡到 OnGround 状态 规则,设置 过渡-基于状态中序列播放器的自动规则 勾选
为 Land 到 OnGround 状态 添加第2个过渡规则 第1个过渡规则 负责转换动画状态 第2个过渡规则 负责指定何时提前过渡
设置第2个过渡规则,如果着陆动画播放大于0.25秒,并且地面速度大于0,则结束着陆动画。
连接 主状态机 Main States
使动画姿势适应非平面地形。
逆运动学是一种来自机器人学的方法,但它在动画中也经常使用,这是一种求解方程的方法,以便我们可以移动骨骼上的特定骨骼。
在脚骨上画一个假想的球体,然后我们将那个球体推到地面上。 我们称之为球体轨迹。 将球体推到地面实际上根本不需要任何时间,它发生在单个游戏帧中,Trace 能够检测命中。
假设地板上的这一点在世界 Z 坐标中为零
另一条腿的 Z 位置为 50
因此,我们需要将角色推低 50 个单位,然后抬起另一条腿。
首先,我们进行球体追踪,并计算脚与其下方表面之间的偏移量。 即 ZOffset_L , ZOffset_R .其中一个将小于另一个,找出哪一个是最低的.
在这种情况下,从 Echo 的右脚到 Z 处地板的球体轨迹等于 0. 该点将具有球体轨迹的最低撞击点。 所以我们将用它来确定我们可以向下移动盆骨的偏移量。
盆骨位于臀部,但所有其他骨头都附着在它上面. 所以如果我们将骨盆向下移动,整个骨骼就会向下移动. 一旦我们将骨盆向下移动了这个量,我们的整个骨骼就会随之移动。 这意味着接触斜坡表面的腿需要向上移动。 我们需要使用逆运动学来定位该腿中的所有其他骨骼,使其自然弯曲。 当我们执行所有这些操作时,我们正在移动骨骼,如果我们立即移动它们,这可能会导致畸形, 需要使用一种称为插值的方法进行更平滑的运动。 逆运动学需要一些复杂的方程。虚幻引擎内置了 IK。所以我们不必解这些方程。 我们只需要移动那根骨头即可。称其为末端骨骼。 附加到它的所有其他骨骼将根据它一起移动。
我们将创建一个专为移动骨骼而设计的资源,这种资产称为 控制绑定 Control Rig。 内容侧滑菜单-右键 -动画-Control Rig-控制绑定
CR_EchoFootIK
CR_EchoFootIK 控制绑定 需要指定骨架。
Echo_Skeleton 骨骼 具由控制 IK的IK骨骼。这些骨骼没有任何实际的网格几何皮肤。
如果没有任何 Ik 骨骼,可以使用虚拟骨骼代替。 首先需要一个虚拟根骨骼作为参考,它将采用与我们的根骨骼相同的变换。
选择根骨骼-当前为root骨骼-添加虚拟骨骼-选择root根 CB root_root 是虚拟骨骼,基于根骨骼
右键 CB root_root -添加虚拟骨骼-选择 foot_l
右键 CB root_root -添加虚拟骨骼-选择 foot_r
移动IK骨骼或虚拟骨骼不会影响网格体。
打开 CR_EchoFootIK 绑定层级 选项 import hierarchy -导入层级-选择 骨骼网格体
控制绑定通过改变骨骼操纵角色
在任何给定时间点,一条腿可能比另一条腿高。 要从两条腿一直向下进行球体追踪,并且球体追踪需要知道一个开始点和轨迹的终点。 选取一根脚骨(IK 或虚拟骨骼)从高点开始跟踪,在低点结束追踪。 因此,我们使用球体从脚的上方向脚下追踪一定量。需要一个函数来处理这个问题。
我的蓝图-创建函数 FootTrace FootTrace将执行这条跟踪
为 FootTrace 添加输入参数 IKFootBone,类型为 绑定元素键 Rig Element Key
为 FootTrace 添加输出 HitPoint ,类型 向量 Vector
展开引脚
为左右脚执行球体追踪的蓝图函数FootTrace
添加一个是否显示跟踪调试的变量 ShouldDoIKTrace
从脚到地面的球体轨迹
插值使偏移平滑
使用较低的脚偏移设置根骨
将插值偏移添加到IK脚骨骼或虚拟骨骼,使其向上或向下移动。
全身IK 确保移动脚时使腿部自然弯曲
整体图
将 CR_EchoFootIK 控制绑定的 变量 ShouldDoIKTrace 设置为公开
打开 ABP_Echo 动画蓝图 添加 Control Rig 为其设置 控制绑定类
选中 Control Rig 细节面板-Control Rig-控制绑定类-CR_EchoFootIK
启用 ShouldDoIKTrace
当角色不在空中时才进行追踪
奔跑时,不应使用IK 如果地面速度为零,则混合姿势使用True节点输入的IK动画,否则使用false节点输入的非IK动画。 缓存 主状态机备用
最终效果
为关卡拖入一块岩石静态网格体
该实例是静态网格物体actor。 并且它有一个静态网格物体组件,并且该静态网格物体组件自动设置了自己的静态网格物体。 一拖入一个自动静态网格物体 Actor,就会创建一个自动静态网格物体 Actor,因为它具有静态网格组件,组件具有碰撞设置,我们可以在详细信息面板中看到这些设置。
静态网格组件碰撞预设
任何能够碰撞的组件都有一个 碰撞预设Collision Presets,定义碰撞行为 打开BP_LearnCharacter蓝图 选择 网格体 组件-细节面板-碰撞-碰撞预设
选择岩石实例-细节面板--碰撞-碰撞预设-自定义custom
所有发生碰撞的组件都有自己的对象类型来确定其他组件如何与该对象类型碰撞交互. 物理体是一种对象类型.
包含 Collision Responses检测响应和Object Responses物体响应. 它们中的每一个都是碰撞对象类型. 在右侧我们可以选择忽略、重叠或阻止该对象类型。 如果世界上的一个组件具有特定的对象类型,那么所有其他组件都将具有他们各自对该对象类型的响应。
在此示例中,我们正在查看对象类型为PhysicsBody物理体的组件的碰撞预设. 因此其他actor可以决定忽略、重叠或阻止对象类型为PhysicsBody物理体的actor。
碰撞对象类型允许我们自定义碰撞行为。
碰撞已启用选项: 1无碰撞 2纯查询(无物理碰撞) 3纯物理(不查询碰撞) 4已启用碰撞(查询和物理) 5仅探测(接触数据,无查询或物理碰撞 ) 6查询和探测(查询碰撞和接触数据,无物理碰撞)
不在物理引擎中创建任何代表。无法用于空间查询(光线投射、扫描、重叠)或模拟(刚体、约束。能达成的最佳性能(尤其对于移动物体而言)
该组件将完全穿过物体或其他物体将穿过它不会有任何响应.不会互相干扰。 不需要物体从它们或类似的东西上反弹
只用于空间查询(光线投射、扫描、重叠)。无法用于模拟(刚体、约束)。适用于角色运动和不需要物理模拟的内容。让数据停留在模拟树外可提升性能。
光线投射是当我们在控制绑定中执行某种跟踪时,我们为每只脚执行球体跟踪,向地面绘制一个假想的球体,并从中得到击中结果,让我们知道这次撞击的位置。这是一个空间查询。
扫描是指对象检测它们是否即将彼此重叠。例如胶囊组件。
重叠是指两个对象实际上彼此重叠的情况。
有多种方法可以检测这些事件何时发生,但我们必须启用空间查询。
纯查询意味着不进行物理模拟。虚幻引擎有一个高性能物理引擎chaos。
只用于物理模拟(刚体、约束)。无法用于空间查询(光线投射、扫描、重叠)。适用于不需要逐骨骼检测的角色上的闪烁位元。 不让数据进入查询树可以提升性能。
当两个模拟物理的物体碰撞时,这允许诸如重力和力之类的东西互相作用在这些物体上,并且它们会相互反弹。
纯物理意味着如果我们执行光线投射则没有查询,扫描。这些东西不会被检测到,因为该组件只是模拟物理。 因此物理引擎正在该组件上工作,这意味着可以模拟重力并向其他物理对象施加力。
可用于空间查询(光线投射、扫描、重叠)和模拟(刚体、约束) 这是两全其美的。 可以进行光线投射和扫描等跟踪。 可以有重力,我们可以有碰撞。 这是最昂贵的,通常用于我们希望能够推动的物体或扔东西。也能够对诸如追踪之类的东西做出反应。例如控制绑定追踪。
仅用于探测物理模拟(刚体、约束)。不能用于空间查询(光线投射扫描、重叠)。当你想要检测潜在的物理交互帮传递接触数据到命中回调或接触修改但不想对这些接触做出物理反应时,此方法非常实用。
可以用于空间查询(光线投射、扫描、重叠和探测物理模拟(刚体、约束)将不允许实际的物理交互,但将生成接触数据,触发命中回调并且接触将出现在接触修改中
更改岩石的 碰撞预设-碰撞已启用 为 无碰撞,对象类型 为WorldStatic
角色将穿过岩石,无碰撞,无追踪,角色IK追踪失效。
角色将穿过岩石,无碰撞,有追踪,角色IK追踪正常。
显示碰撞体积命令 播放时-按下波浪键 ~ ,输入 show collision
角色的碰撞胶囊体与岩石重叠。 胶囊使用空间查询,所以我们希望我们的胶囊被阻止以免与岩石重叠。
岩石没有碰撞组件 右键 岩石 实例-浏览至资产-双击打开 岩石 静态网格体 显示-简单碰撞-将看不到简单碰撞网格。【因为该网格没有简单碰撞】
显示-复杂碰撞- 取消显示
添加 碰撞-添加球体简化碰撞
此时角色将无法穿过岩石,角色与岩石都具有碰撞体积。 角色可站立在岩石上 角色脚IK空间查询追踪与岩石碰撞体重叠,但角色胶囊碰撞体与岩石碰撞体不重叠。
打开岩石网格体-显示简单碰撞,移除碰撞
碰撞-添加26DOP简单碰撞 【贴合岩石外形】
角色与岩石重叠,角色IK追踪也失效。 只有当物体具有空间查询时,我们的胶囊体才能阻止我们穿过物体。 物理不能阻挡物体重叠,只能使用查询阻挡物体重叠。
角色与岩石的碰撞体积不重叠,角色IK追踪正常。
选择 岩石 静态网格体组件-细节面板-物理-模拟物理-启用勾选 角色将可以撞击移动岩石【地板静态网格体也必须使用已启用碰撞(查询和物理)】 这是因为物理引擎允许我们对其施加力。 胶囊正在使用空间查询来阻止事物并阻止我们穿过他们。
由于角色的胶囊体组件已启用碰撞(查询和物理),碰撞预设为 Pawn.能够将物理力传递到其他模拟物理的对象上
岩石默认启用了重力,所以会坠落。如果取消重力,岩石将会漂浮,但依然有物理模拟,可以被撞击移动。
碰撞响应 具由多行选项 细节面板-碰撞-碰撞预设-碰撞响应
BP_LearnCharacter 角色-胶囊体组件-细节面板-碰撞-碰撞预设-对象类型-Pawn
设置 岩石实例-静态网格体-细节面板-碰撞-碰撞预设-碰撞响应-物体相应-Pawn-勾选 忽略项 角色与岩石的查询正常,IK追踪正常。但角色胶囊体碰撞体积与岩石碰撞体重叠。角色胶囊体没有被岩石阻挡。
如果将岩石改为 -碰撞已启用 -纯物理(不查询碰撞) 角色腿部将完全穿过岩石,因为角色IK查询追踪也失效。只有查询可以阻挡物体碰撞体积之间的重叠。
此时相机穿过岩石没有自动变焦效果,需要为岩石启用查询。
为岩石启用 已启用碰撞(查询和物理) 相机经过岩石时会自动变焦跳过岩石
设置 岩石实例-静态网格体-细节面板-碰撞-碰撞预设-碰撞响应-检测响应-camera-勾选 忽略 项 即使岩石启用了查询,它也会忽略相机碰撞通道,这意味着我们没有得到相机放大效果。
例如制作密室暗门, 将迷失墙壁设置为pawn玩家忽略查询,可以通过墙壁,但相机使用查询与墙壁碰撞,不可以通过。除非跟随角色穿墙。
重叠时触发事件, 目标是Actor。
打开蓝图 BP_Item-事件图- 可以看到重叠事件,表示与其他actor重叠时执行该函数。 此actor与另一个actor重叠时调用的事件。例如玩家走进触发器时。如需了解对象拥有阻挡碰撞的事件(如玩家与墙壁发生碰撞),请查看“命中"事件。 Note::在这个和其他actor上组件的bGenerateOverlapEvents都必须设为true,才能生成重叠事件。
BP_Item 默认的碰撞预设为 blockAllDynamic,阻挡全部类型的的对象,角色也无法穿过BP_Item,不能触发BP_Item的重叠事件。 BP_Item 只对 pawn 碰撞对象类型重叠感兴趣。
BP_LearnCharacter 角色-胶囊体的碰撞预设为 Pawn, 已启用碰撞(查询和物理).
检测响应-Visibility-忽略 【控制绑定追踪】
BP_Item -碰撞-碰撞预设-cusetom BP_Item -碰撞-碰撞预设-碰撞响应-物体响应-pawn-勾选 重叠 因此可以使用空间查询,并且可以触发BP_Item 与 pawn的重叠事件
此时相机缩放影响视角
为BP_Item添加静态网格体组件StaticMesh 为StaticMesh选择一个静态网格体【圆球 】
设置 StaticMesh-碰撞预设-custom StaticMesh-碰撞预设-碰撞响应-物体响应-pawn-勾选 重叠 此时与 StaticMesh球体重叠即可触发重叠事件。 删除StaticMesh球体。
为 BP_Item 添加 球体碰撞sphere collision
sphere collision 碰撞预设默认设置为重叠所有动态。 仅具有查询,因此球体组件没有物理特性,但启用了空间查询。 并且它设置为与所有其他对象类型重叠。 所以我们可以继续与球体重叠。触发BP_Item的重叠事件。
显示 BP_Item 的 sphere collision BP_Item-sphere collision组件-细节面板--渲染-游戏中隐藏-取消勾选
可以将 sphere collision 拖离 原网格体
假设只希望在与sphere collision本身而不是 BP_Item金字塔重叠时生成该事件。 为 sphere collision 组件本身设置重叠事件 选择 BP_Item- sphere collision-添加事件- on component begin overlap 事件名称括号的sphere表示只与该球体重叠才触发事件 如果我们与金字塔重叠,什么也不会发生。 但如果我们与sphere collision球体本身重叠,我们的消息就会打印到屏幕上。 它为我们提供了更多的控制权。 这对于拾取世界上的拾取器非常有用。一旦我们真正拾取这些拾取器,这些拾取器就会被销毁。
overlapped component 输出 碰撞体,例如 BP_Item- sphere collision other actor 输出与 sphere collision重叠的对象,例如角色名 other comp 输出与 sphere collision重叠的对象的组件 例如角色的胶囊体碰撞体组件
虚幻引擎有一个复杂的系统来实现观察者模式。 委托是一种特殊类型的类,能够存储观察者列表。 当游戏中发生事件时,主体有能力向每个观察者进行广播。 这涉及循环委托列表并调用观察者对象上的所有回调。
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 组件,缩放其大小
当前物体item mesh碰撞相机时,相机显示不够自然。
打开 BP_Item 蓝图-item mesh 组件-碰撞-碰撞预设-custom
-碰撞-碰撞预设-碰撞响应-检测相应-camera-勾选 忽略 选项
打开 BP_Item 蓝图-item mesh 组件-静态网格体-静态网格体 选择一把剑
actor类包含了放置在世界中的能力。 我们基于 actor 类派生了一个称为 item 的子类,并且添加了我们自己的自定义它的功能。 过创建一个武器类来进一步子类化,并且武器类可以继承item的属性,但随后继续专门化其自己的行为。 该物品能够以正弦运动上下浮动,这样武器就有了很好的浮动效果。 同时具由重叠事件。 如果我们创建一个名为 Weapon 的项目子类,它将继承这些属性。 现在,除了这些功能之外,我还希望能够装备该武器。 将该武器附加到角色上,本质上将网格附加到手上。
如果角色装备了武器,我们应该能够使用该武器。与世界互动、攻击事物并进行战斗。
内容侧滑菜单-all-C++类-Learn-Public-Items-item 右键-创建派生自Item的C++类 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();
};
E:\Unreal Projects 532\Learn\Source\Learn\Private\Items\Weapons\Weapon.cpp
#include "Items/Weapons/Weapon.h"
AWeapon::AWeapon()
{
}
右键 Weapon 类-创建基于Weapon的蓝图类 BP_Weapon
缩放武器使其大小合适 BP_Weapon-Item Mesh-变换-缩放
从BP_Item 事件图表复制波动函数
修改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
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`
```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);
}
将 actor 附件到角色骨骼插槽处
定位到 BP_LearnCharacter 角色骨骼网格体文件 网格体-骨骼网格体资产 打开 echo 骨骼网格体 切换到骨骼树
在骨骼 hand_r 上添加一个插槽,重命名为 RightHandSocket 插槽可以连接物体,插槽会和附着的骨骼一起移动。
RightHandSocket 插槽-右键-添加预览资产-选择剑的网格体
选择 RightHandSocket 插槽 ,调正其位置
右侧-动画-预览控制器-使用特定动画 选择一个动画预览效果 通过下方时间可查看特定位置动画
https://www.mixamo.com/#/ 使用adobe账号登陆
搜索 xbot 角色
点击下载,fbx,t-pose
文件:X Bot.fbx
内容侧滑菜单-导入-选择 X Bot.fbx
该角色文件自身具备骨骼,所以不需要选择骨骼,保持默认空。
点击导入。 忽略平滑组警告。
查看物理资产 X_Bot_PhysicsAsset
在动画栏搜索 axe 下载standing idle空闲动画 with skin 意味着我们将下载网格和动画数据,但我们可以选择不使用,之前下载了角色文件已经拥有的皮肤。 使用 without skin 选项。 将 Standing Idle 动画文件放置到动画文件夹,与之前的角色文件分开。
继续下载动画 standin melee attack.
内容侧滑菜单-导入-选择2个动画-网格体-骨骼 选择之前xbo角色自带的 SKM_Bot 骨骼 导入。
https://docs.unrealengine.com/5.0/zh-CN/ik-rig-in-unreal-engine/
必须下载 Maximo 骨骼网格体【角色自带】是有原因的,因为它是 为这些Maximo 骨骼动画使用的。
打开动画序列。 在动画编辑器的资源详细信息中,我们看到骨架并注意到它呈灰色。 这是因为动画是特定于使用它们的骨骼的。
echo 与 Maximo的骨骼树完全不同。必须通过IK绑定获得Maximo骨骼树。 在虚幻引擎中,为了使一个骨架的动画在另一个骨架上工作,我们执行一种称为重定向的操作,用于将动画从一个骨架重定向到另一个骨架。
可以为骨架网格体创建 IK绑定,用骨骼链将它们映射到另一个骨架网格体的IK绑定。
内容测出啊菜单-右键-动画-重定向-IK绑定 [IK_XBot] 打开 IK_XBot,右侧选择 SK_Bot 骨骼网格体
骨骼 Hips 是当前 Maximo 骨骼树的根骨骼。臀骨。位于顶层。所有其他骨骼都附着在该骨骼上。
骨骼 Hips是重定向root.
选中 Hips -右键-设置重定向根 身体的每个部分都是由骨头链组成的。 如果创建一个新的骨骼链,指定哪些骨骼属于该链,那么一旦创建了一个Echo 的 IK rig,我们可以为 Echo 选择相应的骨骼并为它们创建一个链。这就是我们如何从一个骨架映射到另一个骨架并获取动画数据以进行回显的方法。所以我们这里的工作是创建链并选择每个链中应包含哪些骨骼。
每个IK绑定都需要一个根链。 Maximo 骨骼的根链由一根骨头组成,即我们的臀部骨头Hips。 因此,我们可以右键单击臀部并从选定的骨骼中选择新的重定向链。 选中 Hips -右键-新建重定向链 这个菜单,要求我们命名链并提供起始骨骼和结束骨骼。 命名为 Root.骨骼不变。 添加链。
顺序选择 3个spine 骨骼 -右键-新建重定向链 Spine 之后新建Echo的IK绑定的所有链名必须与此相同。
顺序选择 Neck,Head骨骼 -右键-新建重定向链 Head
左肩为单骨骼链。 选择 LeftShoulder 骨骼 -右键-新建重定向链 LeftClavicle
右肩为单骨骼链。 选择 RightShoulder 骨骼 -右键-新建重定向链 RightClavicle
顺序选择 LeftArm,LeftForeArm,LeftHand 骨骼 -右键-新建重定向链 LeftArm
顺序选择 RightArm,RightForeArm,RightHand 骨骼 -右键-新建重定向链 RightArm
顺序选择左手大拇指1-4,4个骨骼 LeftHandThumb1, LeftHandThumb2, LeftHandThumb3, LeftHandThumb4 骨骼 -右键-新建重定向链 LeftThumb
顺序选择左手食指1-4,4个骨骼 LeftHandIndex1, LeftHandIndex2, LeftHandIndex3, LeftHandIndex4 骨骼 -右键-新建重定向链 LeftIndex
顺序选择中手食指1-4,4个骨骼 LeftHandMiddle1, LeftHandMiddle2, LeftHandMiddle3, LeftHandMiddle4 骨骼 -右键-新建重定向链 LeftMiddle
顺序选择中手食指1-4,4个骨骼 LeftHandRing1, LeftHandRing2, LeftHandRing3, LeftHandRing4 骨骼 -右键-新建重定向链 LeftRing
顺序选择中手食指1-4,4个骨骼 LeftHandPinky1, LeftHandPinky2, LeftHandPinky3, LeftHandPinky4 骨骼 -右键-新建重定向链 LeftPinky
顺序选择 LeftUpLeg,LeftLeg,LeftFoot,LeftToeBase,LeftToe_End 骨骼 -右键-新建重定向链 LeftLeg
顺序选择 RightUpLeg,RightLeg,RightFoot,RightToeBase,RightToe_End 骨骼 -右键-新建重定向链 RightLeg
Maximo 的IK绑定完成
一旦完成,就可以将基于 Maximo 骨架的任何动画传输到 Echo 并使用。
新建 IK 绑定 IK_Echo 选择 echo 骨骼网格体 这里有很多我们的IK绑定中不需要的骨头。 我们只需要与我们在 Maximo IK绑定中制作的链条相对应的链条。
注意Maximo 骨架没有根骨。【高亮部分】,但Echo骨架有根骨root。 果我们选择根骨骼,我们会看到这根骨骼从腿之间开始,一直延伸到盆骨,盆骨实际上是与Maximo臀骨相对应的骨头.
这里也需要一个重定向根,但不是 root骨骼,而是盆骨pelvis 。这与之前的盆骨对应。
盆骨pelvis - 右键-设置重定向根
现在,就像我们在 Maximo IK绑定中所做的那样,我们将拥有一条带有单根骨骼的链,称为Root. 只是这一次我们实际上要使用root骨骼而不是盆骨pelvis。
选中 root -右键-新建重定向链 Root [链名称与Maximo中的链相同]
Echo 上的脊柱有五根骨头,Maximo有3根。 Echo 脊柱链将由这五块骨头组成。 顺序选择 5个spine 骨骼 -spine_01,spine_02,spine_03,spine_04,spine_05,-右键-新建重定向链 Spine
顺序选择 neck_01,neck_02,head骨骼 -右键-新建重定向链 Head
左肩为单骨骼链。 选择 clavicle_l 骨骼 -右键-新建重定向链 LeftClavicle
右肩为单骨骼链。 选择 clavicle_r 骨骼 -右键-新建重定向链 RightClavicle
顺序选择 upperarm_l,lowerarm_l,hand_l 骨骼 -右键-新建重定向链 LeftArm
顺序选择 upperarm_r,lowerarm_r,hand_r 骨骼 -右键-新建重定向链 RightArm
顺序选择左手大拇指1-3,3个骨骼 thumb_01_l, thumb_02_l, thumb_03_l骨骼 -右键-新建重定向链 LeftThumb
这与 Maximo 版本也有点不同. 我们不能使用这些掌骨,否则我们在重定向动画时会遇到一些问题。 所以我们要做的只是选择手指骨骼。 不选择 index_metacarpal_l 这样的位置与Maximo不一致的掌骨,只选择位置与Maximo一致指骨。 所有的其他手指的掌骨都不选择。
顺序选择左手食指1-3,3个骨骼 index_01_l, index_02_l, index_03_l 骨骼 -右键-新建重定向链 LeftIndex
顺序选择中手食指1-3,3个骨骼middle_01_l,middle_02_l ,middle_03_l 骨骼 -右键-新建重定向链 LeftMiddle
顺序选择中手食指1-3,3个骨骼 ring_01_l ,ring_02_l,ring_03_l骨骼 -右键-新建重定向链 LeftRing
顺序选择中手食指1-3,3个骨骼 pinky_01_l ,pinky_02_l,pinky_03_l 骨骼 -右键-新建重定向链 LeftPinky
顺序选择 thigh_l,calf_l,calf_twist_02_l,calf_twist_01_l,foot_l,ball_l 骨骼 -右键-新建重定向链 LeftLeg 这将出现扭曲的骨头,扭曲的骨头会破坏链条。 将导致添加了3条链 将 链 LeftLeg 的 末端骨骼指定为 ball_l, 删除另外2条多余的链
同左腿
Echo 的IK绑定完成
将Mixamo动画重定向至Echo
内容侧滑菜单-右键-动画-重定向-IK重定向器 打开 RTG_XBot
细节面板-源-源IKRig资产-选择 IK_XBot
IK绑定
细节面板-目标-目标IKRig资产-选择 IK_Echo
IK绑定
设置 目标网格体偏移 ,使两个网格体不再重叠
当前他们的姿势不同。 xbot处于T姿势,Echo是A姿势. 如果我们像这样重新定位动画,我们可能会得到不太理想的结果。
可以通过转到资产浏览器来查看重定向动画的样子. 双击相应的动画。
到顶部点击-编辑重定向姿势 角色-骨骼-所有层级
我们会突然看到所有的骨头,并且我们可以移动它们。 现在骨骼本身缩放太大,这是因为在细节中设置了骨骼绘制尺寸。 在编辑姿势模式下,我们可以更改骨骼的姿势,实际上我们可以更改姿势,将其保存为姿势。
创建-新建 现在就有了这两个echo姿势。 如果我们愿意,我们可以切换回默认姿势,并且可以在其中保存多个姿势.为不同目的重新定位资产。
选择 -编辑重定向姿势-
选择上臂骨骼并将其向上移动。调整使echo姿势与xbot相同。
播放动画查看效果。
选择 需要的 动画,点击 导出选定动画 双击 动画序列 预览 此处可以修改插槽位置。 单击时间轴上方可暂停,调整动画时间。
附加剑到右手插槽
修改 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 蓝图 断开 波动函数
选择 Sphere 组件后,在事件图添加事件 On Component Begin Overlap (Sphere) 目标为 Sphere组件。
尝试将 Other Actor 转换为 BP_LearnCharacter,cast to BP_LearnCharacter
武器网格体实际存在于 BP_Weapon 的 Item Mesh 组件。从中取出。 使用 attach component to component [将组件附加到组件]将武器网格体从 Item Mesh 组件 取出,附加到 BP_LearnCharacter的 Mesh组件RightHandSocket 插槽上。 对于变换规则,全部使用 snap to target 对齐到目标 ,将武器贴合插槽。而非使用相对偏移。 最后一个选项表示是否模拟物理。
此时角色靠近武器时可以拾取剑 如果拾取剑较小,可在缩放插槽【待优化】
打开 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);
}
创建操作映射以拾取物体
添加操作Equip 映射按键 E
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);
}
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);
}
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可以拿起武器。
打开 ABP_Echo 动画蓝图 ground locomotion - idle 状态-替换新的待机动画为IK重定向后的 Standing_Idle
添加是否持有武器变量,在空闲状态下,使动画蓝图在未持有武器空闲姿态和持有武器空闲姿态之间切换。 因此需要 角色状态。
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();//从 角色获取角色状态保存
}
}
打开 ABP_Echo 动画蓝图 ground locomotion - idle 状态-替换新的待机动画为IK重定向后的 Standing_Idle 此时,点击 显示继承的变量, 可访问继承自C++的 CharacterState 变量
添加 blend poses 节点【基于ECharacterState】。 它需要一个枚举值,我们可以获取字符状态并将其插入。 混合姿势可以检查角色状态的值,并且可以根据需要返回不同的姿势。 右键点击 blend poses-default pose 添加 新的状态 Unequipped pose, 再添加一个 Equipped One-Handed Weapon
将 Idle 动画 链接 Unequipped pose, 将 Standing_Idle 动画 链接 Equipped One-Handed Weapon. 复制一个 Idle 动画 链接 Default pose,放置人物进入 A-pose
blend poses 节点将检查字符状态的值。
blend poses 节点的 default blend time 表示不同动画姿势之间的转化融合过渡时间
https://www.mixamo.com/#/ 搜索 axe 动画 选择 standing run forward 该角色动画实际上是在向前奔跑,而我们不希望这样。 应该在游戏中自己控制角色的移动。不希望动画推动角色前进。 所以非常重要的是我们在这里选中 in place 复选框,就像我们的其他动画一样原地运行。
点击下载,然后选择不带皮肤,因为我们已经有了骨骼网格体。我们不需要皮肤,我们只需要动画数据。
导入 Standing Run Forward.fbx 由于不自带骨骼,因此选择 mixamo 骨骼 SK_xbot。[之前命名错误,骨骼应使用 SK前缀,骨骼网格体使用SKM]
打开 RTG_XBot IK重定向器
选择 standing run forward 导出选定动画
打开 ABP_Echo 动画蓝图 ground locomotion -run 状态. 添加 standing run forward 动画序列【勾选循环动画】。 添加 blend poses 节点【基于ECharacterState】。 添加 CharacterState 变量
如果我们继续这种简单地将混合姿势节点添加到所有适用的动画蓝图中。 这将需要大量工作。而且它的可扩展性也不是很好。 不应该用一张失控的巨大动画蓝图。 应该有多个动画蓝图,并且知道如何根据正在发生的情况在这些动画蓝图之间进行切换。
链接多个动画蓝图
地面运动状态机和主状态机应该位于它们自己的动画蓝图中
新建动画蓝图 ABP_Echo_MainStates,骨骼选择 Echo_Skeleton
从 ABP_Echo 复制两个状态机
如果编辑器卡住,须在 VS编辑中点击继续
编译 ABP_Echo_MainStates,将显示警告的变量 Character State 右键-创建为新变量
再次编译-警告消失。 根据编译器结果,找到有问题的变量
Is Falling ,ground speed 变量同理。
将 Main States 状态机输出
打开 ABP_Echo。右键添加 linked anim graph 节点 linked anim graph 节点-细节面板-设置-实例类-ABP_Echo_MainStates 动画蓝图 这将 ABP_Echo_MainStates 动画蓝图 连接到该 ABP_Echo 动画蓝图中使用。
打开 ABP_Echo,直接将 Character State 变量拖入 linked anim graph【ABP_Echo_MainStates】可以为其绑定变量。 或者 选中 linked anim graph【ABP_Echo_MainStates】-设置-exposable properties-选择变量
删除其他未使用的空节点。最终如下
新建 ABP__Echo_IK 动画蓝图,骨骼选择 Echo_Skeleton。
打开 ABPEcho 动画蓝图,添加 linked anim graph【ABP_Echo_IK】节点 将 ABPEcho 动画蓝图 的IK控制相关节点全部复制到 ABP__Echo_IK 动画蓝图
ABP__Echo_IK 中删除缓存节点 Main States. 将 Is Falling,Ground Speed 创建变量 新建 input pose 节点 选中 input pose 节点细节面板-输入-名称-重命名为 MainStates
在 ABP__Echo 选择 linked anim graph【ABP_Echo_IK】,右键-刷新节点-可以看到in pose
将缓存 Main States 连接到 linked anim graph【ABP_Echo_IK】输入处。 将 linked anim graph【ABP_Echo_IK】 连接到主输出
将 input pose 节点 缓存为 Main States use cache Main States 分配给 control rig 和 blend pose by bool 的 false.
为 linked anim graph【ABP_Echo_IK】绑定变量 Is Falling,Ground Speed 。
删除多余的IK控制节点。最终如下
主蓝图动画 ABP__Echo 只负责为子蓝图动画传递变量。组织子蓝图动画。
创建攻击动画蒙太奇
状态机是处理动画状态的好方法,因为这些条件会被不断检查,一旦地速大于零,我们就会立即看到姿态的变化。 但对于某些游戏机制,不适合使用状态机,而是使用动画蒙太奇响应游戏中发生的事件的一次性动画。 可以将蒙太奇视为一个或多个动画的一种容器。 例如,我们可能有一个名为“攻击蒙太奇”的蒙太奇,其中包含两个攻击动画。 在蒙太奇中,我们可以创建与蒙太奇中的动画相关的部分,然后通过函数调用来播放蒙太奇,该函数调用指定要播放的蒙太奇以及要跳转到的部分。这对于攻击等一次性动作非常有用。
首先,创建一个新的攻击操作映射。使用鼠标左键。 如果我们要进行操作映射,这意味着我们需要回调,例如攻击函数,我们可以将其绑定到我们的攻击操作映射。 这意味着当我们点击鼠标左键时,我们的攻击函数就会被调用。 攻击函数中会播放动画蒙太奇,为此我们必须访问动画实例。 从敌人实例中,我们可以播放动画蒙太奇,指定要播放哪个蒙太奇。
设置一个Attack操作映射,以便我们可以按下按钮并获得响应中调用的函数。
编辑项目设置,创建攻击操作映射。
打开 BP_LearnCharacter 右键单击并输入Attack,这是我们在制作攻击操作映射后立即创建的操作事件。
https://www.mixamo.com/ 动画选项 搜索 axe , 下载 standing melee attack,standing melee attack 360 动画。均不使用皮肤。
下载 unarmed run forward 。不使用皮肤。勾选 in place。 导入3个动画,骨骼选择 SK_Bot [mixamo骨骼]
打开 RTG_XBot IK重定向器
选择新的3个动画-导出选定动画
右键-动画-动画蒙太奇-选择骨骼-Echo_Skeleton AM_AttackMontage-资产浏览器-将 动画 Standing_Melee_Attack_Horizontal 拖入 时间轴
蒙太奇现在包含一个动画,实际上可以包含多个动画。 拖入 Standing_Melee_Attack_360_High 至 defaultFroup 现在动画蒙太奇可以有多个部分。可以将蒙太奇分段并命名每个部分。
右键 -新建蒙太奇片段 Attack1 将 Default 片段拖离删除,将Attack1片段拖至动画1开头。 右键 -新建蒙太奇片段 Attack2,拖至动画2开头.
打开 蒙太奇片段 点击清除,分开片段,放置全部播放。
蒙太奇可以与称为插槽的东西相关联,插槽只不过是一个标记或标签。 默认情况下,当您创建动画蒙太奇时,它会被分配到默认插槽 DefaultGroup.DefaultSlot。
如果我们尝试播放与给定插槽相关的蒙太奇,我们必须让我们的动画蓝图知道我们正在使用该插槽。
打开 ABP_Echo 动画蓝图,从 ABP_Echo_IK 节点输出拖出 slot[DefaultSlot]节点。并且该节点被分配到默认槽位。 可以创建具有不同名称的自定义插槽,稍后我们将这样做。
现在我们只需让我们的姿势通过默认插槽运行就足够了。 它会覆盖我们角色的姿势。
动画蓝图是一个原子实例,可以通过角色访问动画实例。 打开 BP_LearnCharacter 事件图 拖入 角色 Mesh 组件,从Mesh组件获取动画实例get anin instance。访问动画实例的 函数 montage play.
Montage Play 允许我们选择要播放的蒙太奇。可以从下拉列表中选择一项。选择 AM_AttackMontage
Montage Play 有许多输入,包括播放速率。可以选择更快或更慢地播放动画,将其保留为以常规速度播放。
return value type :默认情况下,Montage Play返回蒙太奇的长度。
in time to start montage at :Montage Play还可以指定开始的时间点。
stop all montages:意味着当这个函数被调用时,如果我们已经在播放其他的蒙太奇,它会停止那些并播放当前这个。
所以当按下鼠标左键时,这个输入动作将响应。我们将从网格中获取 Adam 实例,并调用蒙太奇播放来指定新的攻击蒙太奇。 现在我们没有指定蒙太奇部分,因此它将默认播放第一个蒙太奇部分 Attack1。 现在可以按鼠标左键,现在 Echo 会播放攻击动画。
可以从动画实例get anin instance 拖出 montage jump 跳转播放蒙太奇片段 节点,输入片段 Attack2
section name:输入片段 Attack2
montage:选择资产 AM_AttackMontage
当按下鼠标左键时,将播放Attack2.
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 动画蒙太奇
更换跑步武器动画 https://www.mixamo.com/ 动画栏,搜索 sword run,下载 动画 sword and shield run ,不包含皮肤,勾选 in place。
导入动画,选择 SK_Bot【mixamo】骨骼. 使用 RTG_XBot IK重定向器导出该动画。 可以在RTG_XBot IK重定向器中新建RunPose修正跑步姿势后,再导出动画。
打开 ABP_MainStates -ground locomotion -run 状态机 勾选循环动画。
如果我们已经在攻击,就不能攻击。 要决定是否应该能够攻击,我们需要跟踪角色的状态。
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);
}
}
在攻击动画完成后立即进行攻击。
打开攻击蒙太奇 AM_AttackMontage 在攻击蒙太奇中,我们有两个攻击部分。 当攻击动画结束时。将该ActionState行动状态重置回空闲状态,然后我们就可以再次攻击。
再 通知-1 右键 -添加通知-新建通知 将通知 AttackEnd 放置dao到片段1的末尾,但不能靠近片段2. 当播放此动画部分时到达此攻击结束并通知。 可以从角色动画蓝图中访问它。
通过右上角-更多-进入主动画蓝图ABP_Echo
右键 添加 事件 AnimNotify_AttackEnd 攻击动画播放到结尾将触发该事件。重置动作状态。
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 设置为 未装备状态
打开攻击蒙太奇 AM_AttackMontage,为片段添加通知 AttackEnd.
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() 设置为 未装备状态 从 LearnCharacter 输出节点拖出 调用函数 AttackEnd
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;
}
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);
}
}
为动画添加声音
导入 wav声效
打开 Standing_Melee_Attack_360_High 动画序列 将时间轴移动到剑挥出的地方
在标签栏1-右键-添加通知-播放音效
选中该 playsound-细节面板-动画通知-音效-选择-whoos音效 播放动画可听到音效 删除该音效。
打开 AM_AttackMontage 在轨道1定位武器挥出时机,右键-添加通知-播放音效
选中该 playsound-细节面板-动画通知-音效-选择-whoos音效
添加新轨道 whoosh,将音效移动到该轨道
复制音效 whoosh,移动到动画片段2.
双击音效文件可修改音效属性,但会影响使用该音效的2个动画片段。所以虚幻有了sound cue
右键-音频-ssound cue SC_Whoosh 打开 SC_Whoosh 将Whoosh音频拖入该界面,连接到输出即可。
打开 AM_AttackMontage-选中该 playsound-细节面板-动画通知-音效-选择-SC_Whoosh
首先启用 MetaSound 插件
右键 -新建-MetaSound源 sfx_Whoosh
打开 sfx_Whoosh
点击-输入 加号,添加输入 命名为Whoosh
类型选择-WaveAsset
将输入 Whoosh 拖入面板 右键添加-声波播放器 Wave Player 用以播放输入
为 输入 Whoosh 选择 默认值-默认-whoosh 音波
右键添加 random float 随机(浮点),音高在0-1之间随机。连接 音高频移偏移
右键添加 random float 随机(浮点),音高在0-1之间随机。与 输出声道 相乘
打开 AM_AttackMontage-选中该 playsound-细节面板-动画通知-音效-选择-sfx_Whoosh
新建 MetaSound ,sfx_Exert 新建输入 Exert
类型为-WaveAsset-勾选为数组
默认值-默认-添加10个不是同的音波 每次都会随机播放其中之一。
在面板拖入输入 Exert, 右键添加-shuffle(WaveAsset:Array) shuffle输出声波,需要声波播放器 右键-添加-wave player
添加 2个random 使音调,音高随机
打开 AM_AttackMontage-新建Exert轨道,添加2个sfc_Exert 音效
新建 MetaSound ,sfx_Rock_Run 新建输入 RockRun, 类型为-WaveAsset-勾选为数组.
选中多个音效,直接拖入 RockRun面板- 默认值-默认
勾选 shuffle 的 启用共享状态,将优化共享数据,避免复制。
打开动画序列 Sword_And_Shield_Run 添加音轨 RockRun_L ,RockRun_R, 在脚着地时添加通知-动画通知-音效-选择-sfx_Rock_Run
设置初始IK骨骼位置【罗圈腿】 打开 控制绑定 CR_EchoFootIK 预览场景-动画-预览控制器-使用特定动画-Standing_Idle
姿势为罗圈腿
这与IK骨骼有关。
断开 向前解析 后,腿部正常,IK骨骼没有跟随腿部。 解决这个问题的方法是我们可以对脚骨进行变换并使用该位置来设置。
从 foot_l获取平移,来设置ik_foot_l 右键 transforms-get transform 右键 transforms-set transform
现在 ik_foot_l 骨骼连接到了 真实的foot_l 脚骨根部
设置右脚
连接到执行序列,修复罗圈腿。
www.mixamo.com 在动画栏-搜索 axe 。 下载动画 standing disarn over shoulder 收剑动画。unarmed equid over shoulder拔剑动画。不需要皮肤,【依旧是xbot角色】 导入动画,选择 SK_Bot 骨骼
重命名为 Equip,Unequip
打开 RTG_XBot IK重定向器 ,导出这2个动画。
选中 动画序列 Equip-右键-创建-创建动画蒙太奇-AM_Equip
打开 AM_Equip 动画蒙太奇-资产浏览器,右侧将 Unequip动画序列拖入时间轴到片段2
新建片段 Equip,Unequip。 蒙太奇片段-清除。
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;
}
打开 BP_LearnCharacter 蓝图,-细节面板-Montages-Equip Montage-选择 AM_Equip 动画蒙太奇
打开 BP_LearnCharacter 蓝图 ,选择 网格体 Mesh组件,浏览至骨骼网格体资产
打开 Echo 骨骼网格体 右键-spine_05骨骼-添加插槽 SpineSocket
SpineSocket 插槽-右键-添加预览资产-dag
调整插槽的位置,通过预览动画 equip。
当剑到后被位置时-右键-添加通知-新建通知
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()
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()
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;
}
}
}
收剑或拿剑动画结束时,添加 FinishEquippin 通知,将状态改为
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()
打开 AM_Equip 蒙太奇动画,新建 EquipSound轨道,在抽剑放剑处新建播放音效 PlaySound
新建 MetaSound源 sfx_Shink
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()//播放声音的位置
);
}
}
拾取武器后应设置碰撞预设为 无碰撞
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);
}
}
慢动作命令 :slomo 0.1
0.1倍速运行游戏
调整时间线到问题动画之前
选择要调整的骨骼 upperarm_r 右臂
点击 关键帧 以添加一个关键帧
这将添加 upperarm 曲线 如果我们双击它,我们会看到上臂及其变换的各个方面都在这里,例如平移和旋转,并且它被锁定到动画中的该点。
返回到主时间线 栏
将时间轴拖到问题动画之后的一段时间点上。 点击 关键帧 以添加一个关键帧。 现在这里有两个关键帧锁定这些位置。 现在,这还没有改变任何事情。 我想要的是改变上臂在与罐碰撞的地方的旋转。
将时间线移动到问题动画处,必须在之前2个关键帧之间。 此时可以调整相关的骨骼,upperarm_r。旋转 upperarm_r使其正常。
点击 关键帧 以添加一个关键帧。
我们移动的坐标发生变化。 双击曲线 移动的帧为折线。从开头正常帧回到最后一个关键帧。
回到主时间轴,预览动画,完成。
你可能想要一个没有任何关键帧的动画,在这种情况下,你可以烘焙这个为新的动画。
点击创建资产-创建动画-当前动画-动画数据 新动画将没有关键帧。
为武器添加碰撞盒组件 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());
}
打开武器蓝图 Weapon Box(Weapon Box)(盒体碰撞) 通常用于简单碰撞的盒体。边界在编辑器中被渲染为线条。 源:继承的( C++ ) 引入:Weapon 原生组件名称: Weapon Box 移动性:中可移动 仅编辑器: False
默认WeaponBox盒体碰撞为方形,与剑的形状不一致。 但是,不应该设置 WeaponBox盒体碰撞的变换-缩放。因为所有附加到WeaponBox上的物体也会收到缩放影响。 缩放应保持为1.
应当设置WeaponBox盒体碰撞的属性,即 盒体范围,
对于球体碰撞盒子,同样不设置缩放,而是设置半径。
调整WeaponBox的盒体范围,使其外形与剑一致
BP_Weapon 事件图
选中 WeaponBox 组件【选中才能添加特定事件】 为WeaponBox添加事件 on component begin overlap(WeaponBox)
仅用于调试。
线条追踪 line
形状追踪 shape
盒子追踪 box
打开 BP_Weapon ,在剑头,剑末添加2个场景组件 Start,End
事件图 添加 盒体追踪【通道】box trace by channel
一旦物体与某物重叠则执行盒体追踪。与物体碰撞出绘制绿色盒子。 [碰撞双方都必须有碰撞体,启用重叠事件,否则盒体追踪查询无效] half size 为框半径。 draw debug type 设置为 针对时长
从 命中结果out hit 拖出 break hit resuit 提取数据节点
将另2个 BP_LearnCharacter 拖入关卡。 默认情况下,角色 BP_LearnCharacter 的碰撞胶囊体具由重叠属性。剑会集中角色的胶囊体。 BP_LearnCharacter 的碰撞胶囊体组件碰撞预设为pawn.查询和物理。 对象类型为pawn.
BP_LearnCharacter 的 网格体Mesh组件碰撞预设为CharacterMesh.纯查询。 对象类型为pawn.
BP_Weapon 的 WeaponBox组件碰撞预设为 OverlapAllDynamic。纯查询。 对象类型为WorldDynamic.
更改 BP_Weapon 的 WeaponBox组件碰撞预设为 custom. 忽略 pawn. BP_LearnCharacter的胶囊体与网格体的碰撞对象类型均为pawn,不会与BP_Weapon发生重叠事件。
更改其他角色在关卡的实例的碰撞预设。 将被打击的一个 BP_LearnCharacter 实例的网格体组件的碰撞预设改为自定义。 对象类型改为 WorldDynamic. 勾选 生成重叠事件。 此时击打该角色可触发重叠事件,但不会触发击中box trace跟踪。
这是因为BP_Weapon的box trace跟踪只对visibility可视性通道跟踪。
如果其他对象设置为忽略visibility可视性,将无法对其进行box trace跟踪。 而 被击打的BP_LearnCharacter 实例的网格体组件的碰撞预设的检测响应-visibility默认为忽略。需将visibility改为阻挡。
现在 BP_Weapon的box trace跟踪对BP_LearnCharacter 实例的网格体生效。【打击点绘制球体】
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 位置
设置被击打的BP_LearnCharacter 实例的网格体组件的碰撞预设的检测响应-visibility改为阻挡。
TArray 可以自动扩容
只在攻击时触发重叠事件. 控制 weapon box 组件的重叠事件何时发生.
可以在攻击动画中使用动画通知。 禁用碰撞,直到攻击时启用.攻击动画结束然后立即禁用该碰撞。
打开动画蒙太奇 AM_AttackMontage
添加轨道 EnableCollision 。
在武器击打开始时新建通知 EnableBoxCollision.
添加轨道 DIsableCollision。 在武器击打结束时新建通知 DisableBoxCollision.
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,事件图表。实现新的动画通知 EnableBoxCollision ,DisableBoxCollision。 事件中调用C++的SetWeaponCollisionEnabled,传入参数 查询,和空。
现在,只有攻击时才会触发盒体的碰撞。
使用接口处理受击。
处理用剑击打物体以及处理受击响应。 武器上的盒子组件将触发重叠事件,然后我们进行盒子跟踪以查看我们击中某物的确切点。 这个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方法。
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函数添加默认功能。
// 这里不会定义任何纯虚拟函数
enemy character class 实现 HitInterface
新建敌人character类 Enemy 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);
}
下载 敌人角色模型 https://www.mixamo.com/#/?page=1&type=Character Character 栏:下载 圣骑士 paladin j nordstrom. Paladin J Nordstrom.fbx
导入 Paladin J Nordstrom.fbx。 不选择 骨骼,因为 该角色自带骨骼。 整理材质
打开动画序列 Paladin_J_Nordstrom_Anim ,该动画序列只有一个 T-pose,没有实际动画。所以删除 动画序列 Paladin_J_Nordstrom_Anim。
骨骼网格体 命名为 SKM_Paladin。 物理资产 命名为 PA_Paladin 骨骼 命名为 SK_Paladin。
动画会移动角色网格体的世界坐标。
https://www.mixamo.com/#/?page=1&query=idle+sword+and+shield&type=Motion%2CMotionPack 在 动画栏,搜索 idle sword and shield
下载动画 sword and shield idle。第一个动画包含皮肤。
打开 SKM_Paladin 骨骼网格体。【没有root根骨骼,不能使用虚幻 根运动动画】 SKM_Paladin 的骨骼层级最顶层为臀骨 Hips.位于身体中间,是实际骨骼。
而 角色 Echo 的骨骼层级最顶部为 root 根骨骼。位于角色脚底部平面。用于 根运动动画。
root 根骨骼连接并控制臀骨pelvis。
在虚幻中,根运动动画只能用于带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。都不选皮肤。
将带有皮肤的动画 Sword And Shield Idle.fbx 重命名为 idle with skin.fbx
下载 mixamo_converter 为压缩包
mixamo_converter是为添加根骨骼到骨骼网格物体或具有骨骼网格物体的动画而设计的。 它是专门为我们可以使用 Maximo 的根部运动动画而设计的。
安装插件 mixamo_converter 打开 blender-编辑-偏好设置-插件-安装-选择压缩包即可
启用插件
删除所有元素。 右侧打开 mixamo 工具栏。
convert single 可以导入并转换单个动画。
batch convert 可以导入并转换多个动画。
点击 input 选择 没有根动画的文件夹,
点击输出选择根动画输出文件夹。
展开高级选项,取消勾选应用旋转 apply rotation. 取消 tansfer totation.[置灰]
点击 batch convert 批量转换。batch convert 变灰时完成。
删除 不带root根骨骼的 PA_Paladin,SK_Paladin,SKM_Paladin。 只保留材质文件夹。
首先导入 带根骨骼,带网格体皮肤的动画 idle with skin.fbx 。 不选择骨骼。自带骨骼。
默认没有关联材质。
打开 骨骼网格体 idle_with_skin。 已经包含了 root根骨骼,位于底部地平面。root连接控制臀骨Hips. 此时骨骼网格体可以使用根运动动画。
切换到 资产详情 选项卡,分配材质 M_Paladin
重命名 骨骼网格体 idle_with_skin 为 SKM_Paladin
删除默认白色材质
重命名 物理资产 为 PA_Paladin 重命名 动画序列 idle_with_skin_Anim 为 idle 重命名 骨骼 为 SK_Paladin。
现在,它还不是一个根运动动画,但重点是我们的角色本身必须有一个根骨骼。因为动画是特定于骨骼的。
导入剩余动画,选择骨骼 SK_Paladin. 取消勾选 导入网格体。因为我们没有要导入的网格。我们只要动画。
双击它们,我可以看到它们实际上正在移动角色。这些是根运动动画。 如果我们没有根骨骼,我们无法使用动画本身来移动我们的角色。但现在这些可以。 重命名为 ReactFromBack,ReactFromFront,ReactFromLeft,ReactFromRight.
打开 BP_Enemy-网格体组件-网格体-骨骼网格体资产-选择骨骼网格体 SKM_Paladin
使用 网格体组件:动画资产 预览
调整 骨骼网格体
拖入关卡 BP_Enemy 武器可以命中敌人
使敌人对受击做出响应。 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);
}
}
}
在敌人受击点绘制球体。
新建 AM_HitReact 动画蒙太奇-选择 骨骼 SK_Paladin
打开 AM_HitReact 动画蒙太奇,为 DefaultGroup.Default 蒙太奇轨道,从右侧资产浏览器选择添加4个受击动画序列
在个动画序列起始处添加对应蒙太奇片段名称 FromFront,FromBack,FromLeft,FromRight. 删除默认片段名, 右侧 蒙太奇片段-清除
骨架-选择 骨骼 SK_Paladin
现在我们的 Paladin 应该播放它的空闲动画,但是如果我们播放使用默认插槽的蒙太奇,该蒙太奇将接管,我们将看到该蒙太奇部分在我们的角色上发挥作用。
BP_Enemy-网格体组件-动画-动画模式-使用动画蓝图 BP_Enemy-网格体组件-动画-动画类-ABP_Paladin
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-Montages-hit react montage-AM_HitReact
此时攻击敌人,敌人受击动画播放完后会被拉回到原位置。 此时没有使用到根运动动画。
打开 动画序列 ReactFromLeft -资产详情-根运动-启用根运动 勾选
返回标量 angle between wo vectors 通过点积和反余弦两个矢量之间的角度
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);
}
返回向量 计算角度的正负。
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);
}
基于打击角度选择动画蒙太奇
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);
}
忽略击中的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();
}
}
新建 MetaSound源 sfx_HitFlesh
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
随听众距离衰减 右键-音频-音效衰减 SA_HitFlesh
打开 SA_HitFlesh
衰减函数:它被设置为线性等数学函数来确定声音的行为 衰减形状:默认设置为球形,但您也可以更改该形状。基本上这意味着我们将有一个球体,一个围绕声源的不可见球体。 内部半径:这个球体将有一个半径。这个内半径属性决定了我们在听到最大音量的声音之前可以接近多远。因此默认情况下它设置为 400,这意味着如果我们距离声源 400 单位或更少,我们将听到音源的全部音量。 衰减距离:距离之外将听不到声音。
打开 sfx_HitFlesh
sfx_HitFlesh-源 标签-衰减-衰减设置-SA_HitFlesh
受击后生成流血粒子。 虚幻引擎有两个不同的系统来创建视觉效果。 它有cascade,也有niagara。 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
下资 paragon:Minions 资产 虚幻争霸:小兵 Epic Games - Epic内容 - 2018/03/09 只允许在基于虚幻引擎的产品中使用。包含《虚幻争霸》的小兵和丛林野生生物的角色模型、动画和皮肤。
打开 动画蒙太奇 AM_AttackMontage 选中一个片段 动画片段-播放速率-2 当我这样做时,我注意到我对蒙太奇部分的攻击被向前推进了。 将不得不重新定位所有动画通知。
新建Trails 轨道用于武器追踪
右键-添加通知状态-尾部 trails 尾部通知状态有一个持续时间。 选中 trails -细节面板-动画通知-PS模板-搜索 trail
首个插槽名称 和 第二个插槽名称 定义尾迹的位置。 打开 Echo_Skeleton 骨骼
在 hand_r 添加插槽 FirstTrailSocket 调整 FirstTrailSocket 到剑刃下部 在 hand_r 添加插槽 SecondTrailSocket 调整 SecondTrailSocket 到剑尖
打开 动画蒙太奇 AM_AttackMontage 选中 Trail通知状态-动画通知 首个插槽名称-FirstTrailSocket 第二个插槽名称-SecondTrailSocket
复制一份 Trail通知状态 到动画片段2
using chaos physics to break pots 用混沌物理学破坏罐子
下载一个陶瓷罐子模型pot 首先要创建一个文件夹Destructibles来保存我们想要的项目的所有几合体集。
几合体集基本上是我们在断裂网格后获得的一组静态网格 选择罐子后-点击 新建 指定文件夹Destructibles-创建几合体集
fracture :有不同的断裂方法,使用不同的算法随机地将这个网格分解成小块。
选择 罐子 -点击 -fracture-簇
选择 罐子 -点击 -fracture-统一
点击 破裂 应用该效果 选择部分0,可以多次点击 破裂 应用效果。
它们的子部分取决于所受伤害的程度。 这些可破坏的网格物体基于内部损坏系统,并且造成的损坏基于取决于多种因素。 例如,如果对该网格体施加一定的力,那么如果它超过了一定的损坏阈值,网格会破裂。
进入选项模式-选择罐子,将其举高,启用重力。播放后会掉落摔碎。
-混沌物理-general-显示骨骼颜色 取消勾选
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
这只是一个actor,包含物理场系统组件. 场系统组件是能够产生影响物理场的组件。
添加 RadialFalloff 径向衰减 组件
打开 BP_FieldSystem 事件图表,添加外力。
set radial falloff:作为外部张力的属性。 field magnitude 与 可破坏的网格-伤害阈值 相关 sphere radius 球体半径将决定物体应该距离多近才能受到力。
对物理场进行了径向衰减,这将产生一种压力。
打开 可破坏的网格 -伤害阈值
将 BP_FieldSystem 拖入 可破坏的网格 旁边,将使其破碎。 如果我们将其移到该半径之外,我们将不会获得那么多的力。
使这些碎片都朝一个方向飞 为 BP_FieldSystem 添加一个 RadialVector 径向向量组件。 对碎片施加 RadialVector 方向的线性力
为 BP_FieldSystem 添加一个FieldSystemMetaDataFilter 场系统过滤组件。 默认场系统会对角色的布料产生影响,需过滤角色。 选择 SystemMetaDataFilter 场系统过滤组件-细节面板-场-object类型-破坏 这将使场系统只对可破坏网格产生力。 将 SystemMetaDataFilter 属性赋予线性力 的 meta data 节点
选择 可破坏物体-细节面板-clustering-启用创建簇 启用后,碎片将聚集。需要更大的力场使其破碎。 不启用,碎片将分散。
使用武器破坏可破坏网格
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
为 BP_Weapon 添加组件 FieldSystem场系统组件,RadialFalloff 径向衰减,FieldSystemMetaDataFilter 场系统过滤,RadialVector 径向向量
FieldSystemMetaDataFilter -细节面板-场-object类型-破坏
设置 罐子 可破坏物体-碰撞- 生成重叠事件
打开 罐子 可破坏物体 蓝图 细节面板-碰撞-尺寸特定数据-索引0-碰撞形态-索引0-隐式类型-胶囊体。【更容易发生碰撞】 定义如何初始化刚体碰撞结构的CollisionType。
新建C++ Actor 类 BreakableActor
为了使用 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
打开 BP_BreakableActor-GeometryCollection组件-细节面板-混沌物理-其他集-选择一个可破坏物体 将 BP_BreakableActor 放入关卡,可击打破碎
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)
{
}
1.c++中定义事件,c++和蓝图中都可以实现(c++必须实现)。
2.执行优先级:
如果蓝图不实现,会执行c++函数实现。 如果蓝图和c++都实现,蓝图会覆盖c++实现从而只执行蓝图实现。
E:\Unreal Projects 532\Learn\Source\Learn\Public\Interfaces\HitInterface.h
public:
// BlueprintNativeEvent 蓝图原生事件,不可以是虚函数 virtual,也不可以是纯虚函数 =0
//现在幕后发生的事情是虚幻引擎将创建一个 C++ 特定版本虚函数
UFUNCTION(BlueprintNativeEvent)
void GetHit(const FVector& ImpactPoint);
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。
当我们从 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);
}
BP_BreakableActor 重命名为 BP_Breakable 打开 BP_Breakable-事件图表-添加事件 GetHit 齿轮图标表示这是从接口继承的函数。 并且当在 C++ 中调用 GetHit 时,该事件就会被触发。 如果需要调用C++的 GetHit_Implementation 实现【默认执行蓝图版本,则不执行C++版本】,需要 蓝图手动调用父版本【C++版本】的GetHit。 在 GetHit 节点右键-将调用添加到父函数 【添加一个调用此函数父项的节点】
现在 击打圣骑士 使用C++版本 GetHit。Enemy。 击打罐子,为空函数。BP_Breakable。
为打碎罐子添加音效 新建 MetaSound源 sfx_PotBreak
打开 BP_Breakable 播放音效
set life span 设置3秒寿命
只在 chaos 粉碎时间时 设置寿命
选择 BP_Breakable-GeometryCollection组件-右键-添加事件-添加 on chaos break event 选择 BP_Breakable-GeometryCollection组件-混沌物理-events- 通知中断.[如为true,该组件将生成其他系统可能订阅的破坏事件]
从可破坏物体生成拾取品 下载 资源包 ancient Treasure
新建拾取音效 MetaSound源 sfx_Treasure
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();
}
}
选择组件 Item Mesh ,细节面板-静态网格体-静态网格体-SM_Chalice 选择根 BP_Treasure,细节面板-sounds-pickup sound-sfx_Treasure 可以添加声音衰减。
将 BP_Treasure 放入关卡
在打破罐子后生成宝藏
防止与所在罐子碰撞。
BP_Treasure-ItemMesh-碰撞预设-custom BP_Treasure-ItemMesh-碰撞预设-碰撞已启用-NoCollision 无碰撞
打开 BP_Breakable 添加节点 spawn actor from class 添加节点 get actor transform 添加节点 make transform 添加节点 add 分割结构体引脚
注意:当前的第二个击打动作偶尔会引起BP_Breakable 的 GetGit无限脚本递归。尚未解决。
UClass 表示该类是一个指针,可以指向一个蓝图类或C++类,后期在蓝图编辑器中指定带网格的具体蓝图 BP_Treasure
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
注意:纯 C++版本没有无限递归问题
使用宝藏泛型,可指定多种宝藏。
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);
}
调整形状/半径,而非缩放。
添加节点 set collision response to channel 在胶囊体的破碎事件中设置胶囊体碰撞响应为忽略 pawn。允许角色穿过遗留的胶囊体。
虚幻引擎 Unreal Engine 5 C++
安装UnrealVS Visual Studio扩展
查找相应Visual Studio版本的扩展,位于\Engine\Extras\UnrealVS\VS_VERSION\UnrealVS.vsix,例如,C:\Program Files\Epic Games\UE5\Engine\Extras\UnrealVS\VS2019\UnrealVS.vsix。
运行 UnrealVS.vsix 文件开始安装,只需双击该文件即可。
UnrealVS扩展应该会自动检测到并定位你的VS版本。请确保安装程序定位到正确的VS版本,并选中该复选框。如一切正常,单击 安装(Install) 按钮继续。 安装完毕后,单击 关闭(Close) 按钮。
运行VS,在 扩展(Extensions) > 管理扩展(Manage Extensions) > 已安装(Installed) 中,你应该可以看到该扩展。
转到 视图(View) > 工具栏(Toolbars) (或 右键单击 Visual Studio工具栏区域),然后选择 UnrealVS 以显示扩展的工具栏。
在VS中删除的文件,不会删除本地磁盘文件
所以需要同时需要在本地磁盘手动再删除,否则编译会报错
关闭错误列表窗口
通常情况下,若代码出错,会自动弹出错误列表(Error List) 。但使用虚幻引擎时,错误列表窗口会误报错误信息。建议在使用虚幻引擎时,禁用错误列表窗口,并使用 输出(Output) 窗口查看实际代码错误。以下是关闭错误列表窗口的步骤。
若已打开 错误列表(Error List) 窗口,先请关闭。
在 工具(Tools) 菜单中打开 选项(Options) 对话框。
选择 项目和解决方案(Projects and Solutions) 并禁用 编译出错时始终显示错误列表(Always show Error List if build finishes with error) 。 点击 确定(OK) 。
以下是其他一些可能有用的配置设置:
关闭 显示非活跃代码块(Show Inactive Blocks) 。如不关闭,文本编辑器中的很多代码块将变灰。前往 工具 > 选项 > 文本编辑器 > C/C++ > 视图) 关闭此设置
设置 禁用外部依赖性文件夹 为 True ,以在 解决方案浏览器(Solution Explorer) 中隐藏不必要的文件夹。在 工具 > 选项 > 文本编辑器 > C/C++ > 高级 中找到 禁用外部依赖项性文件夹 。
关闭 编辑并继续(Edit & Continue) 功能,无需使用。前往 工具 > 选项 > 调试(Debugging) > 编辑并继续(Edit and Continue) 。
打开 智能提示(IntelliSense) 。
编码和行尾
https://learn.microsoft.com/zh-cn/visualstudio/ide/encodings-and-line-breaks?view=vs-2022 可以使用“文件”>“高级保存选项”对话框来确定所需的换行符类型 。 还可使用相同的设置更改文件的编码。
如果在“文件”菜单上看不到“高级保存选项”,则可以添加它。
依次选择 Tools (工具)、Customize(自定义)、 选择“命令”选项卡,选择“菜单栏”单选按钮,然后从相应的下拉列表中选择“文件”。选择“添加命令”按钮。 在“添加命令”对话框的“类别”下,选择“文件”,然后在“命令”列表中选择“高级保存选项”。选择“确定”按钮。 使用“上移”和“下移”按钮将命令移动到菜单中的任意位置。选择“关闭”以关闭“自定义”对话框。 有关详细信息,请参阅自定义菜单和工具栏。