feat: 客户端子弹同步,Player内实现简单的射击Weapon逻辑
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
75
Source/FirstPersonDemo/Surviver_FPS/Battle/ProjectileBase.h
Normal file
75
Source/FirstPersonDemo/Surviver_FPS/Battle/ProjectileBase.h
Normal 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);
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "Surviver_FPS/SurviverGameMode.h"
|
||||
|
||||
#include "Battle/SurviverPlayer.h"
|
||||
#include "SurviverPlayer.h"
|
||||
|
||||
ASurviverGameMode::ASurviverGameMode() {
|
||||
DefaultPawnClass = ASurviverPlayer::StaticClass();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user