实习周报5.30

May 30, 2025

9 mins read

实习周报5.30

委托_生命值和子弹数量更新

委托:是一种观察者模式,也被称为代理,主要用于监听事件或变量的变化。监听者将需要响应的函数绑定到委托对象上,使得委托在触发时调用所绑定的函数。

在UE中,按照委托函数个数分为单播、多播,按照是否可暴露给蓝图分为静态和动态。所以一共四种类型。

射击子弹数量同步到UI上使用动态多播委托

动态多播委托在执行时需要实时在类中按照给定的函数名字查找对应的函数,因此执行速度慢,维护了一个由动态单播委托组成的TArray数组,依托动态单播委托实现。

只有动态多播可以被蓝图绑定,需要加标记BlueprintAssignable。

在武器类中声一个带有两个参数的动态多播委托,传递当前子弹数量和最大子弹数量。

FOnAmmoChanged是一个委托类型,可以将多个处理函数绑定到委托上。

/** 子弹变化 **/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAmmoChanged, int32, AmmoCount, int32, MaxAmmo);
UPROPERTY(BlueprintAssignable, Category = "Events")
FOnAmmoChanged OnAmmoChanged; // 创建委托实例
/** 生命值变化 **/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, int32, Health);
	UPROPERTY(BlueprintAssignable, Category = "Events")
	FOnHealthChanged OnHealthChanged;

在武器类的射击函数中添加OnAmmoChanged.Broadcast(AmmoCount, MaxAmmo); // 用来广播委托

在角色类的TakeDamage受伤函数中添加OnHealthChanged.Broadcast(Health);

在玩家控制器类BeginPlay中绘制UserWidget。

void ADemoPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	if (IsLocalController())
	{
		if(GameplayUserWidgetClass)
		{
			GameplayUserWidget = CreateWidget<UDemoGameplayUserWidget>(this, GameplayUserWidgetClass);
			if(GameplayUserWidget)
			{
				GameplayUserWidget->AddToViewport();
			}
		}
	}
}

在角色控制器类中实现可以调用UserWidget类更新UI的函数,还有一进游戏的初始化UI函数

void ADemoPlayerController::OnAmmoChanged(int32 AmmoCount, int32 MaxAmmo)
{
	if(GameplayUserWidget)
	{
		GameplayUserWidget->UpdateAmmoDisplay(AmmoCount, MaxAmmo);
	}
}
 
void ADemoPlayerController::OnHealthChanged(int32 Health)
{
	if (GameplayUserWidget)
	{
		GameplayUserWidget->UpdateHealthDisplay(Health);
	}
}
void ADemoPlayerController::InitGameplayUserWidget()
{
	ADemoPlayerCharacter* PlayerCharacter = Cast<ADemoPlayerCharacter>(GetPawn());
	GameplayUserWidget->UpdateAmmoDisplay(PlayerCharacter->CurrentWeapon->AmmoCount, PlayerCharacter->CurrentWeapon->MaxAmmo);
	GameplayUserWidget->UpdateHealthDisplay(PlayerCharacter->Health);
}

UserWidget中更新子弹数量函数

void UDemoGameplayUserWidget::UpdateAmmoDisplay(int32 AmmoCount, int32 MaxAmmo)
{
	if(TB_AmmoCount)
	{
		TB_AmmoCount->SetText(FText::AsNumber(AmmoCount));
	}
	if(TB_MaxAmmo)
	{
		TB_MaxAmmo->SetText(FText::AsNumber(MaxAmmo));
	}
}

由于把绑定委托写在角色类BeginPlay中会导致时序问题,所以重写PawnClientRestart函数,确保获得到了控制器之后再进行绑定。

ChangeAmmo和ChangeHealth是绑定到委托上的两个函数。

void ADemoPlayerCharacter::PawnClientRestart()
{
	Super::PawnClientRestart();
	ADemoPlayerController* PC = Cast<ADemoPlayerController>(GetController());
	if(PC)
	{
		PC->InitInputSystem();
		if(IsLocallyControlled())
		{
			PC->InitGameplayUserWidget(); // 调用角色控制器类中初始化UI
			OnHealthChanged.AddDynamic(this, &ADemoPlayerCharacter::ChangeHealth);
			CurrentWeapon->OnAmmoChanged.AddDynamic(this, &ADemoPlayerCharacter::ChangeAmmo);
		}
	}
}
void ADemoPlayerCharacter::ChangeHealth(int32 DelegateHealth)
{
	ADemoPlayerController* PC = Cast<ADemoPlayerController>(GetController());
	if(PC)
	{
		PC->OnHealthChanged(DelegateHealth);
	}
}
void ADemoPlayerCharacter::ChangeAmmo(int32 AmmoCount, int32 MaxAmmo)
{
	ADemoPlayerController* PC = Cast<ADemoPlayerController>(GetController());
	if(PC)
	{
		PC->OnAmmoChanged(AmmoCount, MaxAmmo);
	}
}

网络同步_多人游戏

Server

生命值和子弹数量需要同步给服务器,因为客户端不能自己修改,需要服务器来操作。

以子弹数量为例:

UPROPERTY(ReplicatedUsing = On_RepAmmoCount, BlueprintReadWrite, Category = "Weapon")
	int32 AmmoCount = 25;
UFUNCTION()
void On_RepAmmoCount();
 
void ADemoWeapon::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	DOREPLIFETIME(ADemoWeapon, AmmoCount);
}
void ADemoWeapon::On_RepAmmoCount()
{
	OnAmmoChanged.Broadcast(AmmoCount, MaxAmmo);
}

其次同步开火事件,让服务器处理开火逻辑。

// 开火定时器调用FireWeapon开火逻辑
void StartFire();
UFUNCTION(Server, Unreliable, WithValidation)
void C2SStartFire();
	
// 结束开火
void EndFire();
UFUNCTION(Server, Reliable, WithValidation)
void C2SEndFire();
 
void ADemoWeapon::StartFire()
{
	if(HasAuthority())
	{
		if(CanFire())
		{
			CurrentState = EWeaponState::WS_Firing;
			FireWeapon();
			GetWorld()->GetTimerManager().SetTimer(FireTimerHandle, this, &ADemoWeapon::FireWeapon, FireRate, true);
		}
	}
	else
	{
		C2SStartFire();
	}
}
 
void ADemoWeapon::C2SStartFire_Implementation()
{
	StartFire();
}
 
bool ADemoWeapon::C2SStartFire_Validate()
{
	return CanFire();
}
void ADemoWeapon::EndFire()
{
	if(HasAuthority())
	{
		if(CanFire())
		{
			CurrentState = EWeaponState::WS_Idle;
			GetWorld()->GetTimerManager().ClearTimer(FireTimerHandle);
		}
	}
	else
	{
		C2SEndFire();
	}
}
 
void ADemoWeapon::C2SEndFire_Implementation()
{
	EndFire();
}

Muticast播放开火动画

将播放蒙太奇动画封装成一个函数,方便之后其他操作播放不同的动画。

每次射击时调用Multicast_PlayMontageAnimation,同时播放第一人称动画(仅主控端自己可见)和第三人称(其他人可见)。

UFUNCTION(NetMulticast, Unreliable)
void Multicast_PlayMontageAnimation(UAnimMontage* FPMontage, UAnimMontage* TPMontage);
void ADemoPlayerCharacter::Multicast_PlayMontageAnimation_Implementation(UAnimMontage* FPMontage, UAnimMontage* TpMontage)
{
	if(IsLocallyControlled())
	{
		if(FirstPersonMesh && FPMontage)
		{
			UAnimInstance* AnimInstance = FirstPersonMesh->GetAnimInstance();
			if(AnimInstance)
			{
				AnimInstance->Montage_Play(FPMontage, 1.0f);
			}
		}
	}
	else
	{
		if(ThirdPersonMesh && TpMontage)
		{
			UAnimInstance* AnimInstance = ThirdPersonMesh->GetAnimInstance();
			if(AnimInstance)
			{
				AnimInstance->Montage_Play(TpMontage, 1.0f);
			}
		}	
	}
}

武器和角色同步

武器只有一把,在自己视角中是附加到第一人称手臂上,在其他玩家视角是附加到全身模型角色的手上,在网络同步中更新了位置。

// 同步枪械位置到其他客户端的第三人称模型身上视角
UFUNCTION(NetMulticast, Unreliable)
void MulticastWeaponState(FVector NewLocation, FRotator NewRotation);
	
void UpdateCharacterVisibility();
 
void ADemoPlayerCharacter::BeginPlay()
{
	Super::BeginPlay();
	UpdateCharacterVisibility(); // 更新可见性
	if (DefaultWeaponClass)
	{
		CurrentWeapon = GetWorld()->SpawnActor<ADemoWeapon>(DefaultWeaponClass);
		if (CurrentWeapon && FirstPersonMesh)
		{
			CurrentWeapon->SetOwner(this);
			if(IsLocallyControlled())
			{
				CurrentWeapon->AttachToComponent(FirstPersonMesh, FAttachmentTransformRules::SnapToTargetIncludingScale, TEXT("GridPoint"));
			}
			else
			{
				MulticastWeaponState(CurrentWeapon->GetActorLocation(), CurrentWeapon->GetActorRotation());
			}
 
		}
	}
}
 
void ADemoPlayerCharacter::UpdateCharacterVisibility()
{
	if(IsLocallyControlled())
	{
		FirstPersonMesh->SetVisibility(true);
		ThirdPersonMesh->SetVisibility(false);
		LegMesh->SetVisibility(true);
	}
	else
	{
		FirstPersonMesh->SetVisibility(false);
		ThirdPersonMesh->SetVisibility(true);
		LegMesh->SetVisibility(false);
	}
}
 
void ADemoPlayerCharacter::MulticastWeaponState_Implementation(FVector NewLocation, FRotator NewRotation)
{
	if(CurrentWeapon)
	{
		CurrentWeapon->SetActorLocation(NewLocation);
		CurrentWeapon->SetActorRotation(NewRotation);
		CurrentWeapon->AttachToComponent(ThirdPersonMesh, FAttachmentTransformRules::SnapToTargetIncludingScale, TEXT("TPGridPoint"));
	}
}

射线检测_开火

使用自定义武器射线碰撞,因为默认的ECC_Visibility会将摄像机视野中的物体都检测,会命中很多没必要检测的对象,自定义通道提升效率,减少无意义的检测计算。

在项目设置中的Collision中添加新的射线通道,block(阻挡,射线击中就停止)和ignore(忽略,比如忽略队友)和overlap(重叠,触发器盒子,范围检测)

这些逻辑包括TakeDamege都在服务器上判断

void ADemoWeapon::FireRaycast()
{
	ADemoPlayerCharacter* PlayerCharacter = Cast<ADemoPlayerCharacter>(GetOwner());
	if(!PlayerCharacter) return;
	FVector Start = PlayerCharacter->FirstPersonCamera->GetComponentLocation();
	FVector ForwardVector = PlayerCharacter->FirstPersonCamera->GetForwardVector();
	FVector End = (ForwardVector * 1000.f) + Start;
 
	FHitResult HitResult;
	FCollisionQueryParams CollisionParams;
	CollisionParams.AddIgnoredActor(GetOwner());
	// 改成自定义武器射线碰撞
	bool bHit = GetWorld()->LineTraceSingleByChannel(HitResult, Start, End, (ECollisionChannel)TR_WeaponTrace, CollisionParams);
	if (bHit)
	{
		AActor* HitActor = HitResult.GetActor();
		if(HitActor)
		{
			ADemoPlayerCharacter* HitCharacter = Cast<ADemoPlayerCharacter>(HitActor);
			if(HitCharacter)
			{
				HitCharacter->TakeDamage(Damage);
			}
		}
	}
}

Sharing is caring!