feat: 客户端子弹同步,Player内实现简单的射击Weapon逻辑

This commit is contained in:
2025-12-06 19:55:03 +08:00
parent 7bc8c57760
commit 83e7924776
15 changed files with 296 additions and 20 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -41,7 +41,8 @@ void UHealthComponent::BeginPlay() {
* @brief 当CurrentHealth属性被复制时在客户端上调用。
*/
void UHealthComponent::OnRep_CurrentHealth() {
// 在客户端上当CurrentHealth被复制时调用
// 在客户端上当CurrentHealth被复制时调用
UE_LOG(LogTemp, Log, TEXT("[Client]UHealthComponent::OnRep_CurrentHealth(), CurrentHealth: %f"), CurrentHealth);
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
}
@@ -63,7 +64,7 @@ void UHealthComponent::HandleDamage(float DamageAmount) {
// 仅在服务器上处理伤害逻辑
if (GetOwner()->HasAuthority()) {
CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.0f, MaxHealth);
UE_LOG(LogTemp, Log, TEXT("[Server]UHealthComponent::HandleDamage(), CurrentHealth: %f"), CurrentHealth);
// 在服务器上直接广播事件
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);

View File

@@ -0,0 +1,80 @@
// Copyright Majowaveon Games. All Rights Reserved.
#include "ProjectileBase.h"
#include "DamageableInterface.h"
#include "Components/SphereComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
// 构造函数
AProjectileBase::AProjectileBase() {
// 网络同步
bReplicates = true;
PrimaryActorTick.bCanEverTick = false;
AActor::SetReplicateMovement(true);
SetNetUpdateFrequency(60.0f);
SetMinNetUpdateFrequency(30.0f);
// =========================================================
// 1. 碰撞体设置
CollisionComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComp"));
CollisionComp->InitSphereRadius(10.0f);
CollisionComp->SetCollisionProfileName("Projectile");
CollisionComp->SetNotifyRigidBodyCollision(true); // 必须开启以触发 Hit 事件
// 这里很关键:如果是物理模拟子弹,客户端也可以开启碰撞以进行预测,
// 但实际伤害逻辑只由服务器判定。
CollisionComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
RootComponent = CollisionComp;
// 2. 网格设置
ProjectileMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
ProjectileMesh->SetupAttachment(CollisionComp);
ProjectileMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 网格无碰撞
// 设置相对缩放,根据你的模型调整
ProjectileMesh->SetRelativeScale3D(FVector(0.5f));
// 3. 移动组件设置
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileComp"));
ProjectileMovement->UpdatedComponent = CollisionComp;
ProjectileMovement->InitialSpeed = 2000.f; // 速度提高到3000使其看起来更像子弹
ProjectileMovement->MaxSpeed = 2000.f;
ProjectileMovement->bRotationFollowsVelocity = true; // 子弹朝向跟随速度方向
ProjectileMovement->bShouldBounce = false;
// 3秒后自动销毁服务器销毁后客户端也会自动销毁
InitialLifeSpan = BulletLifeSpan;
}
void AProjectileBase::BeginPlay() {
Super::BeginPlay();
// 仅在服务器绑定碰撞事件
if (HasAuthority()) {
CollisionComp->OnComponentHit.AddDynamic(this, &AProjectileBase::OnHit);
// 忽略发射者(防止子弹生成时直接炸到自己)
if (GetInstigator()) {
CollisionComp->IgnoreActorWhenMoving(GetInstigator(), true);
}
}
}
void AProjectileBase::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,
FVector NormalImpulse, const FHitResult& Hit) {
if (!HasAuthority()) return;
if (OtherActor != nullptr && OtherActor != this && OtherActor != GetInstigator()) {
// 1. 基类默认行为:单体伤害
IDamageableInterface* DamageableActor = Cast<IDamageableInterface>(OtherActor);
if (DamageableActor) {
DamageableActor->ReceiveDamage(BaseDamage, GetInstigator());
}
// 2. 播放特效
Multicast_OnImpact(Hit.Location, Hit.ImpactNormal.Rotation());
// 3. 根据配置决定是否销毁
if (bDestoryOnHit) {
Destroy();
}
}
}
void AProjectileBase::Multicast_OnImpact_Implementation(FVector HitLocation, FRotator HitRotation) {
}

View File

@@ -0,0 +1,75 @@
// Copyright Majowaveon Games. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ProjectileBase.generated.h"
class UProjectileMovementComponent;
class USphereComponent;
UCLASS()
/**
* @brief 可被网络复制的投射物基类,具有简单移动、碰撞检测、造成伤害逻辑
* @ingroup Battle
* @note 这个类比 APawn 轻,简单逻辑的敌人可以直接派生自这个类,只需要再挂一个 @ref UHealthComponent
*/
class FIRSTPERSONDEMO_API AProjectileBase : public AActor {
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AProjectileBase();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// 碰撞组件
UPROPERTY(VisibleDefaultsOnly, BlueprintReadWrite, Category = "Projectile")
USphereComponent* CollisionComp;
// 客户端能看到的子弹模型
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Projectile")
UStaticMeshComponent* ProjectileMesh;
// 移动组件
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Projectile")
UProjectileMovementComponent* ProjectileMovement;
protected:
/** 伤害值 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
float BaseDamage = 20.0f;
/** 子弹生命时长 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
float BulletLifeSpan = 3.0f;
/** 碰撞后是否销毁 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
bool bDestoryOnHit = true;
/**
* @brief 仅在服务器触发的碰撞回调
*/
UFUNCTION()
virtual void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,
FVector NormalImpulse, const FHitResult& Hit);
/**
* @brief 多播通知所有客户端播放击中特效(粒子、声音)
* NetMulticast: 服务器调用,服务器+所有客户端执行
* Unreliable: 这种瞬时特效允许在网络极差时丢包,不阻塞网络
*/
UFUNCTION(NetMulticast, Unreliable)
void Multicast_OnImpact(FVector HitLocation, FRotator HitRotation);
/**
* @brief 蓝图实现的特效逻辑(播放声音、生成粒子)
*/
UFUNCTION(BlueprintImplementableEvent, Category = "Visual")
void BP_PlayImpactEffects(FVector Location, FRotator Rotation);
};

View File

@@ -3,7 +3,7 @@
#include "Surviver_FPS/SurviverGameMode.h"
#include "Battle/SurviverPlayer.h"
#include "SurviverPlayer.h"
ASurviverGameMode::ASurviverGameMode() {
DefaultPawnClass = ASurviverPlayer::StaticClass();

View File

@@ -2,7 +2,7 @@
#include "SurviverPlayer.h"
#include "HealthComponent.h"
#include "Battle/HealthComponent.h"
#include "GameFramework/Actor.h"
#include "Animation/AnimInstance.h"
@@ -13,6 +13,7 @@
#include "InputActionValue.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "FirstPersonDemo.h"
#include "Battle/ProjectileBase.h"
/**
* @brief ASuriverPlayer的构造函数
@@ -21,7 +22,7 @@ ASurviverPlayer::ASurviverPlayer() {
// 网络同步设置
bReplicates = true;
GetCharacterMovement()->SetIsReplicated(true);
// 允许该角色每帧调用Tick()
PrimaryActorTick.bCanEverTick = true;
@@ -32,15 +33,16 @@ ASurviverPlayer::ASurviverPlayer() {
GetCapsuleComponent()->InitCapsuleSize(55.f, 96.0f);
// Create the first person mesh that will be viewed only by this character's owner
CharacterMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("角色 Mesh"));
CharacterMesh->SetupAttachment(GetMesh());
// CharacterMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("角色 Mesh"));
// CharacterMesh->SetupAttachment(GetMesh());
// CharacterMesh->SetOnlyOwnerSee(true);
// CharacterMesh->FirstPersonPrimitiveType = EFirstPersonPrimitiveType::FirstPerson;
CharacterMesh->SetCollisionProfileName(FName("NoCollision"));
// CharacterMesh->SetCollisionProfileName(FName("NoCollision"));
// Create the Camera Component
CharacterCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("First Person Camera"));
CharacterCameraComponent->SetupAttachment(CharacterMesh, FName("head"));
// CharacterCameraComponent->SetupAttachment(CharacterMesh, FName("head"));
// CharacterCameraComponent->SetupAttachment(this->GetMesh(), FName("head"));
CharacterCameraComponent->
SetRelativeLocationAndRotation(FVector(-2.8f, 5.89f, 0.0f), FRotator(0.0f, 90.0f, -90.0f));
CharacterCameraComponent->bUsePawnControlRotation = true;
@@ -97,6 +99,9 @@ void ASurviverPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ASurviverPlayer::LookInput);
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this,
&ASurviverPlayer::LookInput);
// Firing
EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Started, this, &ASurviverPlayer::DoStartFiring);
EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Completed, this, &ASurviverPlayer::DoStopFiring);
}
else {
UE_LOG(LogFirstPersonDemo, Error,
@@ -167,3 +172,65 @@ void ASurviverPlayer::ReceiveDamage(float DamageAmount, AActor* InstigatorActor)
void ASurviverPlayer::OnPlayerDied() {
UE_LOG(LogTemp, Warning, TEXT("你死了"));
}
void ASurviverPlayer::DoStartFiring() {
UE_LOG(LogTemp, Log, TEXT("[Client] Starting Firing - Role: %d, RemoteRole: %d"), (int32)GetLocalRole(), (int32)GetRemoteRole());
bIsFiring = true;
Fire();
}
void ASurviverPlayer::DoStopFiring() {
UE_LOG(LogTemp, Log, TEXT("[Client] Stop Firing"));
bIsFiring = false;
GetWorldTimerManager().ClearTimer(FireTimerHandle);
}
void ASurviverPlayer::Fire() {
if (!bIsFiring) return;
// 客户端获取准确的射击位置和方向
FVector SpawnLocation = CharacterCameraComponent->GetComponentLocation();
FRotator SpawnRotation = CharacterCameraComponent->GetComponentRotation();
UE_LOG(LogTemp, Log, TEXT("[Fire] Calling RPC_ServerFire - HasAuthority: %d, Location: %s, Rotation: %s"),
HasAuthority(), *SpawnLocation.ToString(), *SpawnRotation.ToString());
// 把客户端的准确位置和方向传给服务器
RPC_ServerFire(SpawnLocation, SpawnRotation);
// 设置下次射击时间
GetWorldTimerManager().SetTimer(FireTimerHandle, this, &ASurviverPlayer::Fire, FireRate, false);
}
void ASurviverPlayer::RPC_ServerFire_Implementation(FVector SpawnLocation, FRotator SpawnRotation) {
UE_LOG(LogTemp, Warning, TEXT("[Server] RPC_ServerFire called - HasAuthority: %d, Location: %s, Rotation: %s"),
HasAuthority(), *SpawnLocation.ToString(), *SpawnRotation.ToString());
if (!ProjectileClass) {
UE_LOG(LogTemp, Error, TEXT("[Server] ProjectileClass 未设置,无法射击"));
return;
}
// 在服务器生成子弹
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = GetInstigator();
AProjectileBase* SpawnedProjectile = GetWorld()->SpawnActor<AProjectileBase>(ProjectileClass, SpawnLocation, SpawnRotation, SpawnParams);
if (SpawnedProjectile) {
UE_LOG(LogTemp, Log, TEXT("[Server] Projectile spawned successfully: %s"), *SpawnedProjectile->GetName());
} else {
UE_LOG(LogTemp, Error, TEXT("[Server] Failed to spawn projectile!"));
}
// 通知所有客户端播放特效
MulticastPlayFireEffects();
}
void ASurviverPlayer::MulticastPlayFireEffects_Implementation() {
// 播放射击音效、粒子特效等
// 例如:
// UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation());
// UGameplayStatics::SpawnEmitterAttached(MuzzleFlash, CharacterMesh, MuzzleSocketName);
}

View File

@@ -3,7 +3,7 @@
#pragma once
#include "CoreMinimal.h"
#include "DamageableInterface.h"
#include "Battle/DamageableInterface.h"
#include "GameFramework/Character.h"
#include "SurviverPlayer.generated.h"
@@ -11,6 +11,7 @@ struct FInputActionValue;
class UInputAction;
class UCameraComponent;
class UHealthComponent;
class AProjectileBase;
/**
* @class ASurviverPlayer
@@ -21,9 +22,9 @@ UCLASS()
class FIRSTPERSONDEMO_API ASurviverPlayer : public ACharacter, public IDamageableInterface {
GENERATED_BODY()
/** Pawn mesh: first person view */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
USkeletalMeshComponent* CharacterMesh;
// /** Pawn mesh: first person view */
// UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
// USkeletalMeshComponent* CharacterMesh;
/** First person camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
@@ -70,6 +71,19 @@ protected:
/** Set up input action bindings */
virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;
/** 射击 Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* FireAction;
/** 处理开始射击输入 */
UFUNCTION(BlueprintCallable, Category="Input")
void DoStartFiring();
/** 处理结束射击输入 */
UFUNCTION(BlueprintCallable, Category="Input")
void DoStopFiring();
public:
/**
@@ -102,4 +116,34 @@ public:
* @param InstigatorActor Actor
*/
virtual void ReceiveDamage(float DamageAmount, AActor* InstigatorActor) override;
// 射击相关属性
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon")
TSubclassOf<AProjectileBase> ProjectileClass;
UPROPERTY(EditDefaultsOnly, Category = "Weapon")
float FireRate = 0.1f; // 射击间隔
private:
FTimerHandle FireTimerHandle;
bool bIsFiring = false;
/**
* @brief RPC
* @param SpawnLocation
* @param SpawnRotation
*/
UFUNCTION(Server, Reliable)
void RPC_ServerFire(FVector SpawnLocation, FRotator SpawnRotation);
/**
* @brief
*/
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayFireEffects();
/**
* @brief
*/
void Fire();
};

View File

@@ -10,10 +10,10 @@ void UBattleHUD::UpdateHealth(float CurrentHealth, float MaxHealth) {
if (HealthBar) {
HealthBar->SetPercent(CurrentHealth / MaxHealth);
}
if (HealthText) {
HealthText->SetText(FText::FromString(FString::Printf(TEXT("%.0f / %.0f"), CurrentHealth, MaxHealth)));
}
UE_LOG(LogTemp, Log, TEXT("[Client]UBattleHUD::UpdateHealth(): Health: %.2f"), CurrentHealth);
}
void UBattleHUD::NativeConstruct() {