射击轨迹
2023年8月11日 2024年1月11日
概览
Line Trace
- 获取Socket变换信息
- 获取游戏角色Camera组件的变换信息
- 射击游戏对发射子弹的建模
- 完成轨迹计算并获取碰撞信息
- 绘制子弹轨迹和交点
- 为轨迹计算添加屏蔽Actor
Socket
- 之前我们使用Socket作为锚来附加武器
- 现在我们使用它来标记枪口, 获取变换信息
为网格体Rifle添加Socket标记枪口, 获取变换信息
虚幻编辑器
-
双击打开
Content > ExternalContent > Weapon > Weapons > Rifle
-
选中
Skeleton Tree > RifleRoot
, 右键, 选择Add Socket
, 命名为MuzzleSocket -
在细节面板设置MuzzleSocket的位置
Relative Location
默认在武器坐标系的0坐标, 调整到枪口位置 -
更换观察视角, 检查MuzzleSocket变换参数
-
Top视角
-
模拟射击: 绘制子弹轨迹
C++
STUBaseWeapon
- | ||
---|---|---|
起点 | 枪口 | MuzzleSocket的位置分量 |
偏移 | 放大的武器朝向 | MuzzleSocket的前进方向: 通过MuzzleSocket的旋转分量, 获取前进向量 |
终点 | 起点 + 偏移 |
获取MuzzleSocket变换以得到线段所需信息
Stub | |
---|---|
MuzzleSocketName | 保存Socket名称 |
TraceMaxDistance | 放大系数, 亦是子弹射击距离; 1 unreal unit = 1 cm |
获取Socket变换
-
加载WeaponMeshComponent时检查其有效性
在BeginPlay中调用; 使用时不必检查
1check(WeaponMeshComponent);
-
获取变换
- 参数: Socket名
- 返回相对于指定座标系的Socket变换, 默认是世界座标系
1const FTransform SocketTransform = WeaponMeshComponent->GetSocketTransform(MuzzleSocketName); // 默认世界坐标系
- 参数: Socket名
获取起点
1const FVector TraceStart = SocketTransform.GetLocation();
获取武器的朝向
1const FVector ShootDirection = SocketTransform.GetRotation().GetForwardVector(); // 单位向量 2// GetRotation返回类型FQuat 3// 在此处FQuat比FRotator更方便, 因为GetForwardVector的存在, 可以获取X轴分量
计算终点
1const FVector TraceEnd = TraceStart + ShootDirection * TraceMaxDistance;
绘制线段
获取World对象
1#include "Engine/World.h" 2 3if(!GetWorld()) return;
绘制线段
参数 | ||
---|---|---|
bPersistentLines | 是否一直存在 | false |
LifeTime | 持续时间, 单位s | 3 |
DepthPriority | 绘制优先级 | 0 |
1#include "DrawDebugHelpers.h" 2 3DrawDebugLine(GetWorld(), TraceStart, TraceEnd, FColor::Red, false, 3.0f, 0, 3.0f);
完整代码
添加数据成员
ShootThemUp: Weapon/STUBaseWeapon.h
1UPROPERTY(VisibleAnywhere, BlueprintReadWrite) 2FName MuzzleSocketName = "MuzzleSocket"; 3 4UPROPERTY(VisibleAnywhere, BlueprintReadWrite) 5float TraceMaxDistance = 1500.0f; // 1 unreal unit = 1cm > 15m
检查WeaponMeshComponent
ShootThemUp: Weapon/STUBaseWeapon.cpp
1// BeginPlay 2check(WeaponMeshComponent);
MakeShot
包含发射子弹的所有逻辑; 如温切斯特连发步枪, 扣一下扳机, 连发数枚子弹, 所以和Fire分开
ShootThemUp: Weapon/STUBaseWeapon.h
protected
1void MakeShot();
ShootThemUp: Weapon/STUBaseWeapon.cpp
1#include "Engine/World.h" 2#include "DrawDebugHelpers.h" 3 4void ASTUBaseWeapon::MakeShot() { 5 if(!GetWorld()) return; 6 7 const FTransform SocketTransform = WeaponMeshComponent->GetSocketTransform(MuzzleSocketName); 8 const FVector TraceStart = SocketTransform.GetLocation(); 9 const FVector ShootDirection = SocketTransform.GetRotation().GetForwardVector(); 10 const FVector TraceEnd = TraceStart + ShootDirection * TraceMaxDistance; 11 DrawDebugLine(GetWorld(), TraceStart, TraceEnd, FColor::Red, false, 3.0f, 0, 3.0f); 12}
- 在Fire中调用
在虚幻编辑器中查看
-
点击鼠标左键, 触发Fire
-
轨迹与枪口垂直: 未设置MuzzleSocket旋转信息, 需要与世界座标系对应轴方向一致
配置MuzzleSocket旋转信息
Rifle > Details
-
当前
-
前进方向对应X轴方向, 使MuzzleSocket座标系绕自己的Y轴逆时针旋转90度
Relative Rotation > Pitch = 90 -
向上方向对应Z轴方向, 使MuzzleSocket座标系绕自己的X轴顺时针旋转90度
Relative Rotation > Roll = -90 -
效果图
模拟射击: 绘制相交点
C++
给出子弹轨迹的起点和终点, 计算场景中与轨迹碰撞的物体, 保存到FHitResult对象中
HitResult存放碰撞结果
- 包含以下信息
- 数据成员 碰撞时间 交点 ImpactPoint 法向量 指向碰撞Actor的指针 碰撞标志 bBlockingHit … - 需要交点信息
计算与轨迹发生碰撞的Actor
- Engine提供多个碰撞计算函数, 我们使用UWorld::LineTraceSingleByChannel
- LineTraceSingleByChannel返回true则有碰撞, false则无; 而HitResult中亦有bBlockingHit
- 如果发生碰撞, LineTraceSingleByChannel通过函数参数返回与线段第一个发生碰撞的物体信息
- | |
---|---|
ECollisionChannel::ECC_Visibility | 阻挡了Visibility的物体才参与轨迹计算 |
1FHitResult HitResult; 2GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECollisionChannel::ECC_Visibility);
绘制相交点
1if (HitResult.bBlockingHit) 2{ 3 DrawDebugSphere(GetWorld(), HitResult.ImpactPoint, 10.0f, 24, FColor::Red, false, 5.0f); 4}
完整代码
ShootThemUp: Weapon/STUBaseWeapon.cpp
1// MakeShot 2 3FHitResult HitResult; 4GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECollisionChannel::ECC_Visibility); 5if (HitResult.bBlockingHit) 6{ 7 DrawDebugSphere(GetWorld(), HitResult.ImpactPoint, 10.0f, 24, FColor::Red, false, 5.0f); 8}
查看相交点
虚幻编辑器
查看游戏角色Capsule组件的碰撞选项
BP_STUBaseCharacter > CapsuleComponent > Collision
Collision Presets为Custom
Trace Responses > Visibility为Block
为Overlap或Ignore时, 不会参与相交计算
射中时, 以交点为圆心绘制球
注意到, 瞄准十字与子弹轨迹不重合
射击游戏中发射子弹建模
C++
- 子弹从枪口射出, 是对真实世界中的射击进行模拟. 射击游戏中, 子弹从Camera组件位置射出. 该建模也适用于VR
Virtual Reality
- 需要将子弹轨迹的起点改为Camera组件的位置
在武器类获取游戏角色Camera组件的变换信息
- 之前旋转游戏角色视角那里, 有提到是通过游戏角色控制器旋转Camera组件完成的; 获取游戏角色Camera组件的变换信息, 亦是通过游戏角色控制器得到
- 可以通过游戏角色获取其所属控制器
- 可以通过武器组件获取其所属游戏角色
- 武器组件生成武器时, 可以为武器设置所属, 即设置武器属于游戏角色
武器 > 游戏角色 > 游戏角色控制器 > 游戏角色Camera组件的变换信息
武器类生成武器时, 设置其属于游戏角色
STUWeaponComponent
- SpawnActor时, 可以通过FActorSpawnParameters设置其所属; 或者调用SetOwner, 武器的Owner和WeaponComponent的Owner相同
- 使用GetOwner函数和Character数据成员均可获取武器类所属游戏角色
1// CurrentWeapon->SetOwner(GetOwner()); 2CurrentWeapon->SetOwner(Character);
获取游戏角色控制器
STUBaseWeapon
1#include "GameFramework/Character.h" 2#include "GameFramework/PlayerController.h" 3 4const auto Player = Cast<ACharacter>(GetOwner()); 5if (!Player) return; 6 7const auto Controller = Player->GetController<APlayerController>(); 8if (!Controller) return;
通过游戏角色控制器获取Camera组件的变换信息: 位置和旋转
- PlayerCameraManager是全局类, 负责管理Camera, 可以直接获取Character的Camera组件
- GetPlayerViewPoint访问PlayerCameraManager类
1FVector ViewLocation; 2FRotator ViewRotation; 3Controller->GetPlayerViewPoint(ViewLocation, ViewRotation);
重新计算子弹轨迹和碰撞信息
- | |
---|---|
起点 | Camera组件位置 |
偏移 | Camera组件旋转 |
1const FVector TraceStart = ViewLocation; 2const FVector ShootDirection = ViewRotation.Vector(); 3const FVector TraceEnd = TraceStart + ShootDirection * TraceMaxDistance; 4 5FHitResult HitResult; 6GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECollisionChannel::ECC_Visibility);
绘制子弹轨迹和交点
- 子弹从Camera组件所在射出; 若击中, 到击中物体结束, 否则, 绘制射程
- 绘制子弹轨迹时, 起点仍是枪口, 终点为击中物体或射程终点
- 若击中物体, 绘制交点
1if (HitResult.bBlockingHit) 2{ 3 DrawDebugLine(GetWorld(), SocketTransform.GetLocation(), HitResult.ImpactPoint, FColor::Red, false, 3.0f, 0, 3.0f); 4 DrawDebugSphere(GetWorld(), HitResult.ImpactPoint, 10.0f, 24, FColor::Red, false, 5.0f); 5} 6else 7{ 8 DrawDebugLine(GetWorld(), SocketTransform.GetLocation(), TraceEnd, FColor::Red, false, 3.0f, 0, 3.0f); 9}
完整代码
ShootThemUp: Components/STUWeaponComponent.cpp
1// SpawnWeapon 2CurrentWeapon->SetOwner(Character);
ShootThemUp: Weapon/STUBaseWeapon.cpp
1#include "GameFramework/Character.h" 2#include "GameFramework/PlayerController.h" 3 4// MakeShot 5void ASTUBaseWeapon::MakeShot() 6{ 7 if(!GetWorld()) return; 8 9 const auto Player = Cast<ACharacter>(GetOwner()); 10 if (!Player) return; 11 12 const auto Controller = Player->GetController<APlayerController>(); 13 if (!Controller) return; 14 15 FVector ViewLocation; 16 FRotator ViewRotation; 17 Controller->GetPlayerViewPoint(ViewLocation, ViewRotation); 18 19 const FVector TraceStart = ViewLocation; 20 const FVector ShootDirection = ViewRotation.Vector(); 21 const FVector TraceEnd = TraceStart + ShootDirection * TraceMaxDistance; 22 23 FHitResult HitResult; 24 GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECollisionChannel::ECC_Visibility); 25 26 if (HitResult.bBlockingHit) 27 { 28 DrawDebugLine(GetWorld(), SocketTransform.GetLocation(), HitResult.ImpactPoint, FColor::Red, false, 3.0f, 0, 3.0f); 29 DrawDebugSphere(GetWorld(), HitResult.ImpactPoint, 10.0f, 24, FColor::Red, false, 5.0f); 30 } 31 else 32 { 33 DrawDebugLine(GetWorld(), SocketTransform.GetLocation(), TraceEnd, FColor::Red, false, 3.0f, 0, 3.0f); 34 } 35}
查看
-
击中
-
未击中: 真实起点是Camera组件位置
-
终点和瞄准十字重合
击中游戏角色时, 交点在骨骼网格体上
虚幻编辑器
当前击中游戏角色时, 交点在Capsule组件上
放大Capsule组件显示
打开BP_STUBaseCharacter, 选择CapsuleComponent, 去到Details > Shape
-
当前
-
设置
- Capsule Half Height 88 Capsule Radius 88
显示Capsule组件
- 打开Console
- 执行命令
1show collision
效果图
轨迹计算时, 忽略游戏角色Capsule组件, 将骨骼网格体考虑在内
设置Capsule组件的碰撞选项
BP_STUBaseCharacter > CapsuleComponent > Details > Collision > Collision Presets
-
当前
TraceResponses > Visibility为Block, Capsule组件参与轨迹计算
-
设置碰撞预设为Pawn
TraceResponses > Visibility为Ignore, Capsule组件不参与轨迹计算
设置骨骼网格体的碰撞选项
BP_STUBaseCharacter > Mesh > Details > Collision > Collision Presets
-
碰撞预设默认为CharacterMesh, TraceResponses > Visibility为Ignore, 骨骼网格体不参与轨迹计算
-
将碰撞预设设置为Custom, 将TraceResponses > Visibility设置为Block, 使骨骼网格体参与轨迹计算
查看
-
可以击中骨骼网格体
-
子弹可以穿过游戏角色两腿之间而不被Capsule组件阻挡
恢复CapsuleComponent的Shape
当前的问题: 射程的起点为Camera组件位置, 理论上, 我们可以打到枪口无法打到的位置
- 和敌人背对背时击中敌人
- 没调出来
- 解决思路: 要求武器朝向和子弹真实轨迹的夹角为锐角
- 没调出来
- 打到自己
BP_STUBaseCharacter > Camera组件 > Details > Transform > Location, 将XYZ置为0
- 解决思路: 轨迹计算时, 屏蔽游戏角色本身
轨迹计算时屏蔽游戏角色本身
C++
设置轨迹计算参数 FCollisionQueryParams
添加屏蔽Actor
ShootThemUp: Weapon/STUBaseWeapon.cpp
1// MakeShot 2 3FCollisionQueryParams CollisionParams; 4// CollisionParams.AddIgnoredActor(Player); 5CollisionParams.AddIgnoredActor(GetOwner());
计算与轨迹发生碰撞的Actor: 修改参数
ShootThemUp: Weapon/STUBaseWeapon.cpp
1// MakeShot 2GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECollisionChannel::ECC_Visibility, CollisionParams);
击中时, 输出骨骼信息
对不同部位造成伤害时, 游戏角色减少的生命值不同; 比如爆头可以导致游戏角色死亡
存放在FHitResult中
C++
ShootThemUp: Weapon/STUBaseWeapon.cpp
1// MakeShot 2UE_LOG(LogBaseWeapon, Display, TEXT("Bone: %s"), *HitResult.BoneName.ToString());
查看日志
虚幻编辑器