斯坦福UE4 + C++课程学习记录 18:十字准星

目录

1. 创建准星UI

 2. 调整发射代码


1. 创建准星UI

        结合之前文章关于UMG的内容,我们可以十分快速地创建一个之子准星的UI,这一部分视频对应课程P20开始。

        首先,我们需要调整一下摄像机的位置。如果我们现在运行关卡,会发现游戏角色位于镜头的正中间,这无疑会在操作中遮挡玩家的实现。回忆一下各种第三人称视角的游戏,人物通常位于画面的偏左或偏右的位置。

        进入Player的蓝图编辑器,选择弹簧臂组件(SpringArmComp),调整其中的“摄像机”属性。通过设置“长度”可以变化摄像机与角色的距离,设置“插槽偏移”从而在改变相机位置时保持弹簧地碰撞检测的功能。这里大家可以根据自己的喜好自行调节视角,我在这个地方的设置如图18-1:

图18-1 弹簧臂设置

        接下来,在UI文件夹下创建Crosshair_Widget控件蓝图,并添加一个图像控件。通常,十字准星固定在屏幕的正中央。因此,我们设置图像的锚点为屏幕中间点,设置位置X、Y为0,并把X与Y的对齐均设置为0.5。适当调节尺寸X、Y属性,使准星的大小合适。

图18-2 准星设置

        回到Player蓝图中,在之前添加血量条的位置再把准星加上,同样添加到视口。然后运行关卡,就可以看到屏幕正中间有一个简单的准星了。

图18-3 添加准星
图18-4 运行测试

 2. 调整发射代码

        在此前第5节的文章中,我在SurCharacter中的PrimaryAttack使用了GetActorRotation函数来获取角色的旋转,从而使粒子以角色的正前方发射,如图18-5:

图18-5 发射粒子

        在添加十字准星后,我们肯定需要粒子沿着准星发射,即沿着玩家的视角发射。所以,我们只用把PrimaryAttack_TimeElapsed函数中的GetActorRotation()换成GetControlRotation()即可。此外,在当前项目下,我使用UE_LOG发现GetControlRotation()和GetViewRotation()的值相等,所以后面就统一用前者。

图18-6 运行测试

        但这样的更改可能会带来两个问题:一是反方向(正脸面向摄像头)发射时可能会打中角色自己,这取决于魔法粒子蓝图的设置;二是击中的位置与准星还是存在偏差,尤其在角色朝向左边的时候(角色右手发射):

图18-7 反向发射
图18-8 位置偏差

        第一个问题很容易理解,反方向发射的魔法粒子检测到了自己角色的actor,就触发了OnActorOverlap事件。在此前第11节的内容中,我们已经使用了Instigator判断是不是玩家自己,所以第一个问题我并没有遇到。

        但此处还要注意,粒子虽然可以穿过角色,但仍会对角色自己造成伤害。我们控制粒子销毁的功能是在蓝图中实现的,而控制伤害的功能是在代码中实现的(有点混乱,但这是课程出于教学目的的设计),所以我们还需要再C++中添加忽略对玩家造成伤害的代码。代码如下所示,其实就是在之前的基础上在第一个if处添加了对Instigator的判断,和蓝图中的逻辑是一样的。

void ASurMagicProjectile::OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	//避免攻击者被自己的粒子伤害
	if (OtherActor && OtherActor != GetInstigator()) {
		//获得AttributeComp
		USurAttributeComponent* AttributeComp = Cast<USurAttributeComponent>(OtherActor->GetComponentByClass(USurAttributeComponent::StaticClass()));
		// 再次判空,可能碰到的是墙壁、箱子等没有血量的物体
		if (AttributeComp) {
			// 魔法粒子造成20血量伤害
			AttributeComp->ApplyHealthChange(-20.0f);
			// 一旦造成伤害就销毁,避免穿过角色继续计算
			Destroy();
		}
	}
}

        第二个问题就稍微复杂一些,我个人是这样理解的:粒子发射的方向是角色的相机方向,而粒子发射的位置是角色右手。也就是说,只要发射位置不在屏幕正中心(也就是相机的位置),最后粒子的落点一定存在偏移,且距离屏幕中间越远偏移越大。

        一种最简单的解决办法是,直接设置魔法粒子的发射点在屏幕正中间,也就是相机位置。这个方法只修改需要一行代码,也就是把相机的Rotation和Location都赋给SpawnTM:

FTransform SpawnTM = FTransform(GetControlRotation(), CameraComp->GetComponentLocation());
图18-9 运行测试

        这种方法的效果如图18-9,毫无疑问实现了指哪打哪。如果要使用这个方法,也许在每次攻击时把角色旋转到朝前,然后要精调角色在镜头中的位置,以及魔法粒子沿着GetControlRotation这个方向的具体生成位置,使其攻击时手部刚好和发射粒子的位置重合,感觉第一人称射击游戏可以更方便地使用这种方法(以上都是个人猜想)。

        另一种很巧妙的方法,是先检测再发射。在发射魔法粒子前,先用射线检测的方法,检测一个从相机位置沿着其朝向的较大的射程范围内,有没有命中对象。如果检测到命中,返回命中的位置,并利用向量加法得到这次攻击的方向向量,然后再发射魔法粒子;如果不命中,最后就会落到沿着相机,距离为射程的那个点上。这种方法每次攻击的方向向量是不相同的,是判定完位置后再计算的。

        实现方法和之前第7节打开箱子的射线检测很类似,这里使用形状检测来增加检测的空间,核心代码如下:

// 获取模型右手位置
FVector RightHandLoc = GetMesh()->GetSocketLocation("Muzzle_01");

// 检测距离为 5000 cm = 50 m
FVector TraceStart = CameraComp->GetComponentLocation();
FVector TraceEnd = TraceStart + ( GetControlRotation().Vector() * 5000 );

// 检测半径
FCollisionShape Shape;
Shape.SetSphere(20.0f);

// 不要检测自己角色
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);

// 碰撞设置
FCollisionObjectQueryParams ObjParams;
ObjParams.AddObjectTypesToQuery(ECC_WorldStatic);
ObjParams.AddObjectTypesToQuery(ECC_WorldDynamic);
ObjParams.AddObjectTypesToQuery(ECC_Pawn);

FHitResult Hit;
if (GetWorld()->SweepSingleByObjectType(Hit, TraceStart, TraceEnd, FQuat::Identity, ObjParams, Shape, Params)) {
    TraceEnd = Hit.ImpactPoint;
}
		
// 尾向量 - 头向量 = 方向向量 eg:起点(0,0) 终点(1,1),方向向量为(1,1)
FRotator ProjRotation = FRotationMatrix::MakeFromX(TraceEnd - RightHandLoc).Rotator();
// 朝向检测到的落点方向,在角色的右手位置生成
FTransform SpawnTM = FTransform(ProjRotation, RightHandLoc);

// 此处设置碰撞检测规则为:即使碰撞也总是生成
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.Instigator = this;

GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, SpawnParams);

        原理已经十分清晰明了,但我使用如上代码在运行时并没有达到预期的效果。在我添加了DrawDebugLine和DrawDebugPoint进行调试后,终于发现问题的原因:预先检测时直接略过了预期对象,根据UE_LOG打印的内容显示,最后传回的Actor是地板Floor,如图18-10中黄球所示:

图18-10 Debug

        于是我顺着这个问题,对碰撞和物理属性的各种设置折腾了许久,最后终于发现把对象的碰撞预设设置为“BlockAll”后,代码就可以正常运行了。但这样的缺点在于,这些物体即使设置了“模拟物理”也不会和场景内的火药桶产生交互,火药桶爆炸后它们还死死地钉在原地。

        接下来我只能从函数上入手,我看到UE对SweepSingleByObjectType的解释是发射一个形状,会返回第一个碰撞的对象。一看到碰撞这个字眼,我就突然意识到ObjParams里面传入了需要检测的对象,兴许是我场景中的对象不属于代码中的三种。我在UE中检查这面墙的对象类型,发现是PhysicsBody,于是我尝试性地在代码中加入了ECC_PhysicsBody。最后,终于在物体能模拟物理的基础上,被SweepSingleByObjectType检测到。最终效果如图18-11所示,其中黄色是形状检测的点,紫色是粒子触发命中事件的点。

图18-11 运行测试

        看来根本的问题还是在UE的碰撞设置上,目前有关各种碰撞预设设置、命中事件、重叠事件等一些列问题理解还十分粗浅。所以暂且留个坑,待日后对UE有一定概念后再回头来系统研究这个问题。

猜你喜欢

转载自blog.csdn.net/surkea/article/details/127363384