实习周报6.6

Jun 6, 2025

8 mins read

实习周报6.6

武器组件化

将武器的功能抽象成一个个独立可复用的模块,然后组合到武器实例上。比如射线枪和后坐力还有换弹都可以抽象成一个个功能,然后自由组合。

组件继承UActorComponent,我写了两个组件分别是子弹管理组件开火组件

子弹管理组件

子弹管理组件定义了子弹数量和初始化子弹数量、判断子弹数量是否满足开火条件、子弹减少等方法。并将之前在武器类中子弹数量变化的代理和子弹数量的网络同步转移到当前子弹管理组件脚本中。

void UAmmoManagerComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(UAmmoManagerComponent, AmmoCount);
}
 
UAmmoManagerComponent::UAmmoManagerComponent()
{
	PrimaryComponentTick.bCanEverTick = true;
	SetIsReplicatedByDefault(true);
}
 
void UAmmoManagerComponent::BeginPlay()
{
	Super::BeginPlay();
	AmmoCount = MaxAmmo;
}
 
void UAmmoManagerComponent::InitializeAmmo(int32 InAmmoCount, int32 InMaxAmmo)
{
	MaxAmmo = InMaxAmmo;
	AmmoCount = InAmmoCount;
}
 
void UAmmoManagerComponent::ExpendAmmoCount()
{
	-- AmmoCount;
}
 
bool UAmmoManagerComponent::EnoughAmmoToFire()
{
	return AmmoCount > 0;
}
 
void UAmmoManagerComponent::SetMaxAmmo(int32 NewMaxAmmo)
{
	MaxAmmo = NewMaxAmmo;
	AmmoCount = FMath::Clamp(AmmoCount, 0, MaxAmmo);
	OnRep_AmmoCount();
}
 
void UAmmoManagerComponent::OnRep_AmmoCount()
{
	OnAmmoChanged.Broadcast(AmmoCount, MaxAmmo);
}

开火组件

开火组件,创建了一个FireBase的基类开火组件,定义了一个OwnerWeapon用于获取当前开火的武器。

void UDemoFireBase::Initialize(ADemoWeapon* InWeapon)
{
	OwnerWeapon = InWeapon;
}

在武器类中的BeginPlay去查找是否存在开火组件,如果存在就调用这个函数。

void ADemoWeapon::BeginPlay()
{
	Super::BeginPlay();
 
	FireComponent = FindComponentByClass<UDemoFireBase>();
	if(FireComponent)
	{
		FireComponent->Initialize(this);
	}
}

投掷类开火和射线开火都是FireBase的派生类,重写开火函数实现多态,游戏运行时会走当前开火组件的逻辑。

射线枪开火组件

void UDemoFireRaycast::Fire()
{
	Super::Fire();
	if(!OwnerWeapon || !OwnerWeapon->CanFire()) return;
	FireRaycast();
}
void UDemoFireRaycast::FireRaycast()
{
	ADemoPlayerCharacter* PlayerCharacter = Cast<ADemoPlayerCharacter>(OwnerWeapon->GetOwner());
	if(PlayerCharacter)
	{
		FVector Start = PlayerCharacter->FirstPersonCamera->GetComponentLocation();
		FVector End = Start + PlayerCharacter->FirstPersonCamera->GetForwardVector() * 1000.f;
		FHitResult HitResult;
		FCollisionQueryParams Params;
		Params.AddIgnoredActor(PlayerCharacter);
		bool bHit = GetWorld()->LineTraceSingleByChannel(HitResult, Start, End, (ECollisionChannel)TR_WeaponTrace, Params);
		if(bHit)
		{
			AActor* HitActor = HitResult.GetActor();
			if(HitActor)
			{
				ADemoPlayerCharacter* HitCharacter = Cast<ADemoPlayerCharacter>(HitActor);
				if(HitCharacter)
				{
					HitCharacter->TakeDamage(Damage); // Damage也是射线组件定义的伤害
				}
			}
		}
	}
}

投掷类榴弹开火组件

protected:
	UPROPERTY(EditDefaultsOnly, Category = "Projectile")
	TSubclassOf<ADemoGrenade> ProjectileClass;
	float LaunchSpeed = 1000.f;
	FName MuzzleSocketName = "FireMuzzleSocket";
 
void UDemoProjectileFire::Fire()
{
	Super::Fire();
	if(!ProjectileClass) return;
	if(OwnerWeapon)
	{
		USkeletalMeshComponent* MeshComponent = OwnerWeapon->FindComponentByClass<USkeletalMeshComponent>(); // 获取武器网格体为了获取枪口位置
		if(MeshComponent)
		{
			FVector MuzzleLocation = MeshComponent->GetSocketLocation(MuzzleSocketName);
			FRotator MuzzleRotation = MeshComponent->GetSocketRotation(MuzzleSocketName);
			ADemoGrenade* SpawnGrenade = GetWorld()->SpawnActor<ADemoGrenade>(ProjectileClass, MuzzleLocation, MuzzleRotation); // 在枪口位置生成手榴弹 
			if(SpawnGrenade)
			{
				UProjectileMovementComponent* ProjectileMovement = SpawnGrenade->FindComponentByClass<UProjectileMovementComponent>(); 
                            // 通过ProjectileMovementComponent来实现榴弹的发射有速度
				if(ProjectileMovement)
				{
					FVector LaunchDirection = MuzzleRotation.Vector();
					ProjectileMovement->Velocity = LaunchDirection * LaunchSpeed;
					ProjectileMovement->Activate();
					ProjectileMovement->OnProjectileStop.AddDynamic(SpawnGrenade, &ADemoGrenade::OnGrenadeStop); // UE自带的监听手榴弹停止的委托 当手榴弹停止运动时爆炸、伤害
				}
			}
		}
	}
}

手榴弹类

ADemoGrenade::ADemoGrenade()
{
 	// 初始化手榴弹组件 根组件是球形碰撞体(设置为BlockAll)挂载了GrenadeMesh(设置为NoCollsion),还有创建了ProjectileMovementComponent 并设置它的弹射 摩擦力等参数
	PrimaryActorTick.bCanEverTick = true;
	SphereCollision = CreateDefaultSubobject<USphereComponent>(TEXT("SphereCollision"));
	SphereCollision->SetSphereRadius(5);
	RootComponent = SphereCollision;
	GrenadeMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("GrenadeMesh"));
	if(GrenadeMesh)
	{
		GrenadeMesh->SetupAttachment(SphereCollision);
	}
	
	ProjectileMoveComp = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
	if(ProjectileMoveComp)
	{
		ProjectileMoveComp->bShouldBounce = true;
		ProjectileMoveComp->Bounciness = 0.4;
		ProjectileMoveComp->Friction = 0.6;
		ProjectileMoveComp->BounceVelocityStopSimulatingThreshold = 0.1;
		ProjectileMoveComp->InitialSpeed = 1000.f;
		ProjectileMoveComp->MaxSpeed = 1000.f;
	}
}
// 手榴弹停止运动事件触发的函数
void ADemoGrenade::OnGrenadeStop(const FHitResult& HitResult)
{
	FVector ExplosionCenter = HitResult.ImpactPoint; // 将投掷中心点传入ApplyExplosionDamage
	MulticastPlayExplosionEffect(ExplosionCenter); // 多播RPC让大家看到爆炸
	ApplyExplosionDamage(ExplosionCenter, ExplosionRadius);
}
 
void ADemoGrenade::ApplyExplosionDamage(FVector ExplosionCenter, float Radius)
{
	TArray<FOverlapResult> OverlapResults;
	FCollisionShape CollisionShape;
	CollisionShape.SetSphere(Radius);
       // 检测球形范围内有无物体
	bool bHit = GetWorld()->OverlapMultiByChannel(OverlapResults, ExplosionCenter, FQuat::Identity, (ECollisionChannel)TR_WeaponTrace, CollisionShape);
	if(bHit)
	{
              // 遍历所有被击中的玩家 让他们受伤
		for(const FOverlapResult& Result : OverlapResults)
		{
			AActor* HitActor = Result.GetActor();
			if(HitActor)
			{
				ADemoPlayerCharacter* HitCharacter = Cast<ADemoPlayerCharacter>(HitActor);
				if(HitCharacter)
				{
					HitCharacter->TakeDamage(Damage);
				}
			}
		}
	}
}
void ADemoGrenade::MulticastPlayExplosionEffect_Implementation(FVector ExplosionCenter)
{
	if(ExplosionEffect)
	{
		UNiagaraFunctionLibrary::SpawnSystemAtLocation(GetWorld(), ExplosionEffect,ExplosionCenter, FRotator::ZeroRotator);
	}
}

玩家复活

玩家重生功能已经可以正常使用了。

在角色类中声明动态多播委托

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerDeath, ADemoPlayerCharacter*, DeadCharacter);
FOnPlayerDeath OnPlayerDeath;

玩家受伤TakeDamage函数中如果生命值<=0时,调用PlayerDead()函数。

void ADemoPlayerCharacter::TakeDamage(int32 DamageAmount)
{
	if(bIsDead) return;
 
	if (HasAuthority())  // 确保只有服务器才会处理伤害
	{
		Health -= DamageAmount;
		OnHealthChanged.Broadcast(Health);
 
		if (Health <= 0)
		{
			Health = 0;
			bIsDead = true;
			// 第一人称死亡蒙太奇没找到合适的
			PlayerDead();
		}
	}
}
void ADemoPlayerCharacter::PlayerDead()
{
	Multicast_PlayMontageAnimation(FPDeadMontage, TPDeadMontage); // 播放死亡动画
	GetCharacterMovement()->DisableMovement(); 
	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	SetActorEnableCollision(false);
	OnPlayerDeath.Broadcast(this); // 重生 参数传入当前玩家
}

在GameMode中重写UE内置的RestartPlayer函数

void ADemoGameMode::RestartPlayer(AController* NewPlayer)
{
	if(NewPlayer == nullptr || NewPlayer->IsPendingKillPending())
	{
		return;
	}
	TArray<AActor*> PlayerStarts;
   // 存储世界中所有的玩家出生点到TArray数组中
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), PlayerStarts);
	if(PlayerStarts.Num() == 0)
	{
		return;
	}
   // 获取随机出生点 调用RestartPlayerAtPlayerStart,传入这个随机出生点
	APlayerStart* RandomStart = Cast<APlayerStart>(PlayerStarts[FMath::RandRange(0, PlayerStarts.Num() - 1)]);
	if(RandomStart)
	{
		RestartPlayerAtPlayerStart(NewPlayer, RandomStart);
		ADemoPlayerCharacter* PlayerCharacter = Cast<ADemoPlayerCharacter>(NewPlayer->GetPawn());
		if(PlayerCharacter)
		{
          // 每次出生都绑定玩家的死亡委托 恢复死亡前的状态
			PlayerCharacter->OnPlayerDeath.AddDynamic(this, &ADemoGameMode::ChangeDeath);
			PlayerCharacter->GetCharacterMovement()->SetMovementMode(MOVE_Walking);
			PlayerCharacter->GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
			PlayerCharacter->SetActorEnableCollision(true);
		}
	}
}

出现的问题:

武器同步问题

beginplay中武器生成之后没办法正确同步,导致要么有客户端无法发射/有时候又正常,要么能发射但看不见手雷。——时序问题

void ADemoPlayerCharacter::BeginPlay()
{
	Super::BeginPlay();
	
	MulticastUpdateVisibility();
	if (DefaultWeaponClass)
	{
		CurrentWeapon = GetWorld()->SpawnActor<ADemoWeapon>(DefaultWeaponClass);
		if (HasAuthority() && DefaultWeaponClass)
		{
			CurrentWeapon->SetOwner(this);
		}
	}
}
void ADemoPlayerCharacter::OnRep_CurrentWeapon()
{
	if(CurrentWeapon)
	{
		if(IsLocallyControlled())
		{
			CurrentWeapon->AttachToComponent(FirstPersonMesh, FAttachmentTransformRules::SnapToTargetIncludingScale, TEXT("GridPoint"));
		}
		else
		{
			CurrentWeapon->AttachToComponent(ThirdPersonMesh, FAttachmentTransformRules::SnapToTargetIncludingScale, TEXT("TPGridPoint"));
		}
	}
}

Sharing is caring!