跳至主要內容

EXP14.单向跳跃平台(碰撞体积)

Mr.Si大约 6 分钟u++

提要

头像
还记得小时候玩的魂斗罗、马里奥这种横板游戏吗?
头像
当然记得啦!
头像
不知道你有没有注意其中的一个细节。
头像
什么细节?
头像
你有没有想过,怎么还原这种从下方穿透、上方可以站立的平台?
头像
正常来说,我们只能从旁边跳上去。
头像
提到这个我们还是先来回顾一下UE中碰撞体积的一些知识

响应通道(Collision Response Channels)


UE 的碰撞系统由三部分组成:

  1. 碰撞通道(Collision Channel)

    • 用于标记对象所属类型。

    • UE 内置两种类型:

      • Object Channels(对象通道):标记对象类别,比如 PawnWorldStaticWorldDynamic
      • Trace Channels(射线通道):用于射线/扫描(LineTrace、SphereTrace)检测。
头像

自定义通道(Project Settings → Collision → Object Channels / Trace Channels)。

  1. 碰撞预设(Collision Presets)

    • UE 提供常用预设组合,比如:

      • BlockAll → 阻挡所有
      • OverlapAll → 全部触发重叠事件
      • Pawn → 阻挡世界静态物体,忽略玩家等
    • 每个预设内部其实就是 对每个通道的响应配置

  2. 碰撞响应(Collision Response)

    • 决定了对象对某个通道的具体行为:
碰撞响应类型描述
ECR_Block阻挡产生物理碰撞,角色/物体无法穿过
ECR_Overlap重叠允许穿过,但会触发 OnComponentBeginOverlap/EndOverlap 事件
ECR_Ignore忽略不会碰撞,也不会触发重叠事件

方案

方案1

头像
我想到的第一个方法是给平台上方加一个触发体积

  • 每个平台上方加一个 TriggerBox/BoxComponent(只用于感知角色是否接近)
  • 平台默认 Pawn 通道忽略阻挡
  • 当角色进入 Trigger → 开启 平台阻挡(ECR_Block)
  • 当角色离开 Trigger → 关闭阻挡(ECR_Ignore)
APlatformBase::APlatformBase()
{
	PrimaryActorTick.bCanEverTick = false;
	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("FootTrigger"));
	Trigger->SetupAttachment(GetRootComponent());
    
	// 设置大小,稍微比脚底大一点
	Trigger->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	Trigger->SetCollisionResponseToAllChannels(ECR_Overlap);
	Trigger->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
	Trigger->OnComponentBeginOverlap.AddDynamic(this, &APlatformBase::OnTriggerOverlap);
	Trigger->OnComponentEndOverlap.AddDynamic(this, &APlatformBase::OnTriggerEndOverlap);
}

void APlatformBase::SetBlocking(bool bEnable)
{
	if (bIsBlocking == bEnable) return;

	bIsBlocking = bEnable;
	GetRenderComponent()->SetCollisionResponseToChannel(ECC_Pawn, bEnable ? ECR_Block : ECR_Ignore);
}

void APlatformBase::OnTriggerOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor && OtherActor->IsA<ACharacter>())
	{
		SetBlocking(true);
	}
}

void APlatformBase::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor && OtherActor->IsA<ACharacter>())
	{
		SetBlocking(false);
	}
}

方案1|缺陷多人问题

头像
如果是多角色阁下怎么应对?
头像
这还不简单,直接加一个计数器,记录一下多少角色触碰了就行了。

// 记录 Trigger 内的角色数量
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Collision")
int32 OverlappingCharacters = 0;

void APlatformBase::OnTriggerOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor && OtherActor->IsA<ACharacter>())
	{
		OverlappingCharacters++;
		SetBlocking(true);
	}
}

void APlatformBase::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor && OtherActor->IsA<ACharacter>())
	{
		OverlappingCharacters = FMath::Max(OverlappingCharacters - 1, 0);
		SetBlocking(false);
	}
}

方案1|缺陷平台过近导致卡死。

头像
事实上这个方案还有一个致命缺陷,两个平台一但非常贴近,就会卡死在缝隙里。

方案2|代码

头像
当然上面的方案是站在平台角度思考的,我这里其实有一种站在角色本身角度思考的方法。
头像
说说看。
头像
反过来,给角色加一个额外的碰撞体积。

void ADoodleJumpCharacter::OnFootTriggerOverlap(
	UPrimitiveComponent* OverlappedComponent,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex,
	bool bFromSweep,
	const FHitResult& SweepResult)
{
	if (!OtherActor) return;

	if (APaperFlipbookActor* Platform = Cast<APaperFlipbookActor>(OtherActor))
	{
		const FVector CharLocation = FootTrigger->GetComponentLocation();
		const FVector PlatformLocation = Platform->GetActorLocation();

		if (CharLocation.Z > PlatformLocation.Z)
		{
			Platform->GetRenderComponent()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);		
			//跳跃
			Jump();

		}
		else
		{
			Platform->GetRenderComponent()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
		}
	}
	
}

方案2|缺陷——跳跃问题

头像
事实上这个方案并不会生效。

为了更加直观,用一张图演示问题:

头像
难怪我的角色跳不起来,明明我已经给了Jump()事件
头像
对于这个问题你觉得该怎么解决?
头像
这还不简单,直接控制器或者角色本身的tick中一直调用跳跃不就行了嘛?
头像
事实上平台有很多派生变化,有些才踩上去会陷下去,有些则会触发弹簧把你弹飞。因此,你的方案并不能一直适用。
头像
这么看来跳跃事件好像还是得交给平台自己来处理?
头像
没错,得让平台自己决定怎么跳。为了方便后续推进,我们先抽离一些平台接口。
//  copyright by sigaohe

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "PlatformInterface.generated.h"

// This class does not need to be modified.
UINTERFACE()
class UPlatformInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 平板接口
 */
class DOODLEJUMPGAME_API IPlatformInterface
{
	GENERATED_BODY()

public:

	//初始化操作
	UFUNCTION(BlueprintNativeEvent,BlueprintCallable)
	void InitAction();

	//触摸操作
	UFUNCTION(BlueprintNativeEvent,BlueprintCallable)
	void TouchAction(AActor* InActor);
	
};

头像
通过接口来通知平台

void ADoodleJumpCharacter::OnFootTriggerOverlap(
	UPrimitiveComponent* OverlappedComponent,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex,
	bool bFromSweep,
	const FHitResult& SweepResult)
{
	if (!OtherActor) return;

	if (APaperFlipbookActor* Platform = Cast<APaperFlipbookActor>(OtherActor))
	{
		const FVector CharLocation = FootTrigger->GetComponentLocation();
		const FVector PlatformLocation = Platform->GetActorLocation();

		if (CharLocation.Z > PlatformLocation.Z)
		{
			Platform->GetRenderComponent()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);		
			if (Platform->GetClass()->ImplementsInterface(UPlatformInterface::StaticClass()))
			{
			IPlatformInterface::Execute_TouchAction(Platform, this);
			}
		}
		else
		{
			Platform->GetRenderComponent()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
		}
	}
	
}
头像

后面我知道!直接LaunchCharacter就行了,平台可以通过控制JumpStrength来控制角色跳跃高度。并且,不用担心覆盖角色自己原本的跳跃属性。

void APlatformBase::TouchAction_Implementation(AActor* OtherActor)
{
	CharacterJump(OtherActor);
	if(JumpSound)
	{
		UGameplayStatics::PlaySoundAtLocation(this,JumpSound, GetActorLocation(),GetActorRotation());
	}
}

void APlatformBase::CharacterJump(AActor* OtherActor)
{
	if (const auto Character = Cast<ACharacter>(OtherActor))
	{
		Character->Jump();
		Character->LaunchCharacter(FVector(0.f, 0.f, JumpStrength), true, true);
	}		
}

头像
不错,但咱们描述的问题依然存在,角色虽然被向上推了,但跳跃动画依然没有播放。
头像
我能想到的方案是给一点延迟,让角色的胶囊体落在平台上,然后再去执行跳跃。
头像
不太靠谱,如果间距远大于我们的延迟落下的距离,依然会吞掉动画。
头像
那该如何是好?
头像

事实上我们可以利用角色的OnLanded事件来解决这个问题。


void ADoodleJumpCharacter::OnFootTriggerOverlap(
	UPrimitiveComponent* OverlappedComponent,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex,
	bool bFromSweep,
	const FHitResult& SweepResult)
{
	if (!OtherActor) return;
	if (const APaperFlipbookActor* Platform = Cast<APaperFlipbookActor>(OtherActor))
	{
		const FVector CharLocation = FootTrigger->GetComponentLocation();
		const FVector PlatformLocation = Platform->GetActorLocation();

		// 条件:脚在平台上方 + 正在下坠
		if (CharLocation.Z > PlatformLocation.Z)
		{
			// 临时允许角色踩在平台上
			Platform->GetRenderComponent()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);
		}
		else
		{
			Platform->GetRenderComponent()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
		}
	}
}
void ADoodleJumpCharacter::Landed(const FHitResult& Hit)
{
	Super::Landed(Hit);

	//判断是不是平台
	if (AActor* LandedActor = Hit.GetActor())
	{
		if (LandedActor->GetClass()->ImplementsInterface(UPlatformInterface::StaticClass()))
		{
			//通知平台接口
			IPlatformInterface::Execute_TouchAction(LandedActor, this);
		}
	}
}
	
头像
秒啊!这样平台碰撞事件和跳跃就解耦了!也能确保落地时才会通知平台触发后续事件了!