一 创建玩家
建立项目
- 创建空项目,导入
Anim Starter Pack项目包 - 在
C++ Classes->CoopGame下创建SCharacter继承Character,设置为Public - 由
SCharacter创建蓝图类BP_SCharater, 基础设置
- 添加
Skeletal Mesh - 设置
Z-Location - 设置
Z-Rotation: -90/270 - 设置动画蓝图
UE4ASP_HeroTPP_AnimBlueprint_C
- 把
BP_SCharater拖到关卡中,设置Auto Possess Player为Player 0,即可开始游戏
添加摄像机
- 添加组件并初始化


SpringArm的作用就是能够以人物为轴点进行旋转,如果单添加摄像机,则摄像机以自己中心点为轴心;
bug: 把组件初始化放进BeginPlay,则蓝图类中不显示; 因为生成蓝图类只会调用构造函数
- 蓝图类中调整
SpringArm位置
输入绑定
先在Project Setting中完成输入绑定

移动
- 声明函数

- 定义函数

- 绑定

视角
- 绑定

蹲伏
- 添加蹲伏绑定
protected:
void BeginCrouch();
void EndCrouch();
-----------------------------
void ASCharacter::CrouchAction()
{
if(IsCrouching)
{
IsCrouching = false;
UnCrouch();
} else
{
IsCrouching = true;
Crouch();
}
}
-----------------------------
// 蹲伏
PlayerInputComponent->BindAction("Crouch", IE_Pressed, this, &ASCharacter::CrouchAction);
- 设置
Character可蹲伏
GetMovementComponent()->GetNavAgentPropertiesRef().bCanCrouch = true;
bug: 设置了bCanCrouch=true,还是不能蹲伏 因为蓝图类中没有还原默认值
跳跃
- 绑定跳跃即可
// 跳跃
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ASCharacter::Jump);
GetMovementComponent()->GetNavAgentPropertiesRef().bCanJump = true;
bug: 绑定没响应,最后发现是Input设置成BackSpace了, 这个是退格不是空格,晕 找半天,,,最后改成Space Bar即可
创建动画
bug: Blueprint Runtime Error: "Access None trying to read property SCharacterRef"
原因:用错事件了

- 移动动画

二 武器1/2
下载资源 特效和枪械
创建武器
- 创建
SWeapon类,添加USkeletalMeshComponent组件
SkeMeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeMeshComponent"));
RootComponent = SkeMeshComponent;
- 创建蓝图类
BP_SWeapon,并指定枪支网格 - 在
SCharacter的网格体中设置WeaponSocket,然后EvnentGraph->BeginPlayer中生成枪支

bug: 运行游戏直接闪退,因为没有绑定Owner,而在代码中又通过GetOwner():AActor来获取持有者,直接程序崩溃
创建弹道
- 从摄像头位置往前发射射线,这个位置需要修正起始位置
起点位置
从眼睛发射
一般发射点都是从眼睛的地方,即GetActorEyesViewPort:
void AActor::GetActorEyesViewPoint( FVector& OutLocation, FRotator& OutRotation ) const
{
OutLocation = GetActorLocation();
OutRotation = GetActorRotation();
}
由于Pawn进行了重写,该方法变成了
void APawn::GetActorEyesViewPoint( FVector& out_Location, FRotator& out_Rotation ) const
{
out_Location = GetPawnViewLocation();
out_Rotation = GetViewRotation();
}
其中GetPawnViewLocation默认为
FVector APawn::GetPawnViewLocation() const
{
return GetActorLocation() + FVector(0.f,0.f,BaseEyeHeight);
}
这里的BaseEyeHeight在蓝图Details中可以设置,对应眼睛的高度
从摄像头处发射
在从眼睛发射基础上进行修改,即重写GetPawnViewLocation即可
FVector ASCharacter::GetPawnViewLocation() const
{
if(CameraComponent)
return CameraComponent->GetComponentLocation();
return Super::GetPawnViewLocation();
}
开火(射线检测)
void ASWeapon::Fire()
{
AActor *MyOwner = GetOwner();
FVector EyeLocation;
FRotator EyeRotator;
MyOwner->GetActorEyesViewPoint(EyeLocation, EyeRotator);
FVector TraceEnd = EyeLocation + EyeRotator.Vector()*10000;
FHitResult OutHit;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(MyOwner);
QueryParams.AddIgnoredActor(this);
QueryParams.bTraceComplex = true;
if(GetWorld()->LineTraceSingleByChannel(OutHit, EyeLocation, TraceEnd, ECC_Visibility, QueryParams))
{
}
DrawDebugLine(GetWorld(), EyeLocation, TraceEnd, FColor::Orange, false, 1, 0, 1);
}
产生伤害
AActor *HitActor = OutHit.GetActor();
// Damage和DamageType定义成变量了,Damage:20
UGameplayStatics::ApplyPointDamage(HitActor, Damage, EyeRotator.Vector(), OutHit, GetOwner()->GetInstigatorController(), this, DamageType);
应用伤害
- 创建敌人
BP_Enemy:Actor,添加Skeletal Mesh - 设置
Coliision->BlockAll - 打开
EventGraph->Event Point Damage

枪口与击中特效
定义变量
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Weapon")
UParticleSystem *MuzzleEffect;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Weapon")
UParticleSystem *ImpactEffect;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Weapon")
FName MuzzleSocketName;
- 添加击中特效
if(ImpactEffect)
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactEffect, OutHit.ImpactPoint, OutHit.ImpactNormal.Rotation(), FVector(0.5), true);
- 添加枪口特效
if(MuzzleEffect)
UGameplayStatics::SpawnEmitterAttached(MuzzleEffect, SkeMeshComponent, MuzzleSocketName);
弹道特效
- 创建粒子系统,光速粒子发射器


可以在Source之间Target延展贴图,其中Source是特效生成位置,Target通过代码中指定位置
2. 代码中定义粒子系统和TracerTargetName
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Weapon")
UParticleSystem *TracerEffect;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Weapon")
FName TracerTargetName;
---------------------------------
TracerTargetName = "Target"; // 在粒子蓝图中,查看Target Name 是不是"Target"
- 设置
TracerTargetLocation
// 初始是射线检测的终点
FVector TracerTragetLocation = TraceEnd;
// 如果命中目标,则更新为检测点
TracerTragetLocation = OutHit.ImpactPoint;
- 生成特效
if(TracerEffect)
{
FVector MuzzleLocation = SkeMeshComponent->GetSocketLocation(MuzzleSocketName);
UParticleSystemComponent *TracerComp = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), TracerEffect, MuzzleLocation);
if(TracerComp)
TracerComp->SetVectorParameter(TracerTargetName, TracerTragetLocation);
}
准星
- 创建
UI/WBP_Collimator,添加Image,将Anchors->按住Ctrl+Shift设置到中心,然后将Alignment都设置为0.5 - 设置
Scale和Color and Opacity - 在
BP_SCharacter中创建并加载到视口

榴弹发射器
- 创建
SProjectileWeapon继承SWeapon,重写Fire函数,然后生成对应蓝图类,并赋值参数
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="ProjectileWeapon")
TSubclassOf<AActor> ProjectileClass;
virtual void Fire() override;
-------------------------------
void ASProjectileWeapon::Fire()
{
AActor *MyOwner = GetOwner();
FVector EyeLocation;
FRotator EyeRotator;
MyOwner->GetActorEyesViewPoint(EyeLocation, EyeRotator);
auto WuzzleLocation = SkeMeshComponent->GetSocketLocation(MuzzleSocketName);
FHitResult OutHit;
FCollisionQueryParams CollisionQueryParams;
CollisionQueryParams.AddIgnoredActor(this);
CollisionQueryParams.AddIgnoredActor(MyOwner);
CollisionQueryParams.bTraceComplex = true;
auto LineEndPoint = EyeLocation + EyeRotator.Vector() * 10000;
auto ProjectileEndPoint = LineEndPoint;
if(GetWorld()->LineTraceSingleByChannel(OutHit, EyeLocation, LineEndPoint, ECC_Visibility, CollisionQueryParams))
{
ProjectileEndPoint = OutHit.ImpactPoint;
}
// 计算榴弹发射朝向
FRotator ProjectileRotator = (ProjectileEndPoint - EyeLocation).Rotation();
FActorSpawnParameters SpawnParameters;
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
if(ProjectileClass)
GetWorld()->SpawnActor<AActor>(ProjectileClass, WuzzleLocation, ProjectileRotator, SpawnParameters);
}
- 创建
Item/BP_Projectile蓝图类,添加StaticMesh和ProjectileMovement组件
StaticMesh设置显示网格;ProjectileMovement设置初始速度 EventGraph中设置榴弹逻辑

三 武器2/2
瞄准
- 定义相关变量
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Player")
bool IsZooming;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Player")
float DefaultFOV;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Player")
float ZoomedFOV;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Player")
float ZoomInterpSpeed;
- 构造函数初始化
ZoomedFOV = 45;
ZoomInterpSpeed = 20;
- 添加
Input->Zoom输入绑定
Super::SetupPlayerInputComponent(PlayerInputComponent);
...
// 瞄准
PlayerInputComponent->BindAction("Zoom", IE_Pressed, this, &ASCharacter::ZoomAction);
}
void ASCharacter::ZoomAction()
{
// UE_LOG(LogTemp, Warning, TEXT("瞄准"));
IsZooming = !IsZooming;
}
Tick中插值渐变FOV
void ASCharacter::Tick(float DeltaTime)
{
...
float TargetFOV = IsZooming ? ZoomedFOV : DefaultFOV;
float NewFOV = FMath::FInterpTo(CameraComp->FieldOfView, TargetFOV, DeltaTime, ZoomInterpSpeed);
CameraComp->SetFieldOfView(NewFOV);
}
武器代码优化,从蓝图到c++
播放特效
void ASWeapon::PlayEffect(FVector TracerTragetLocation, FHitResult OutHit)
{
// 枪口特效
if(MuzzleEffect)
UGameplayStatics::SpawnEmitterAttached(MuzzleEffect, SkeMeshComponent, MuzzleSocketName);
// 弹道特效
if(TracerEffect)
{
FVector MuzzleLocation = SkeMeshComponent->GetSocketLocation(MuzzleSocketName);
UParticleSystemComponent *TracerComp = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), TracerEffect, MuzzleLocation);
if(TracerComp)
TracerComp->SetVectorParameter(TracerTargetName, TracerTragetLocation);
}
// 击中特效
if(OutHit.GetActor() && ImpactEffect)
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactEffect, OutHit.ImpactPoint, OutHit.ImpactNormal.Rotation(), FVector(0.5), true);
}
持有武器并开火
把蓝图中的生成SWeapon并触发Fire,用c++实现
- 定义变量
UPROPERTY(EditDefaultsOnly, Category="Player")
TSubclassOf<class ASWeapon> InitWeaponClass;
UPROPERTY(VisibleAnywhere, Category="Player")
ASWeapon *HoldWeapon;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Player")
FName HoldWeaponSocketName;
bug: 程序崩溃,因为TSubclassOf需要在蓝图中初始化,代码初始化比较繁琐
- 构造函数中初始化
// 武器
HoldWeaponSocketName = "WeaponSocket";
- 装备初始武器
void ASCharacter::BeginPlay()
{
...
EquipWeapon(nullptr);
}
void ASCharacter::EquipWeapon(ASWeapon* Weapon)
{
if(!Weapon)
{
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
Weapon = GetWorld()->SpawnActor<ASWeapon>(InitWeaponClass, FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);
}
HoldWeapon = Weapon;
if(HoldWeapon)
{
HoldWeapon->SetOwner(this);
HoldWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, HoldWeaponSocketName);
}
}
- 开火绑定
添加Input->Fire绑定
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ASCharacter::Fire);
void ASCharacter::Fire()
{
if(HoldWeapon)
HoldWeapon->Fire();
}
开火镜头震动
- 创建
CamerShake蓝图,Blueprint class->CamerShakeBase->DefaultCameraShakeBase, 4.27版本是这个 - 选择
Noise模式

- 代码中持有并调用
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Components")
TSubclassOf<class UCameraShakeBase> CameraShake;
void ASCharacter::Fire()
{
if(HoldWeapon)
{
HoldWeapon->Fire();
auto PC = Cast<APlayerController>(GetController());
if(PC)
PC->ClientStartCameraShake(CameraShake);
}
}
自定义表面
Project Settings->Pyhsics->Physical Surface中添加物理表面

为了访问更已读,在CoopGame.h中宏定义常量
#define SURFACE_FLESHDEFAULT SurfaceType1
#define SURFACE_FLESHVULNERABLE SurfaceType2
注意:主代码中如果引用CoopGame.h或者CoopGame/CoopGame.h
Content/Material中创建两种Physic Material, 并分别指定其Surface Type



- 给
Physic模型身体指定物理材质



- 根据射线检测的物理材质区分,生成不同的特效
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(MyOwner);
QueryParams.AddIgnoredActor(this);
QueryParams.bTraceComplex = true;
QueryParams.bReturnPhysicalMaterial = true; // 返回物理材质 !!!!!!!一定要有
if(GetWorld()->LineTraceSingleByChannel(OutHit, EyeLocation, TraceEnd, ECC_Visibility, QueryParams)) {...}
################################
if(OutHit.GetActor())
{
// OutHit可能没有指定物理材质,通过DetermineSurfaceType会返回默认材质,不可以自己取! OutHit.PhysMaterial.Get()->SurfaceType
EPhysicalSurface SurfaceType = UPhysicalMaterial::DetermineSurfaceType(OutHit.PhysMaterial.Get());
UParticleSystem *SelectedEffect = nullptr;
switch (SurfaceType)
{
case SURFACE_FLESHDEFAULT:
case SURFACE_FLESHVULNERABLE:
SelectedEffect = FleshImpactEffect;
UE_LOG(LogTemp, Warning, TEXT("Flesh"));
break;
default:
SelectedEffect = DefaultImpactEffect;
UE_LOG(LogTemp, Warning, TEXT("Default"));
break;
}
if(SelectedEffect)
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), SelectedEffect, OutHit.ImpactPoint, OutHit.ImpactNormal.Rotation(), FVector(0.5), true);
}
bug: 无法解析的外部符号 "__declspec(dllimport) public: static enum EPhysicalSurface __cdecl UPhysicalMaterial::DetermineSurfaceType(class UPhysicalMaterial const *)" (_imp?DetermineSurfaceType@UPhysicalMaterial@@SA?AW4EPhysicalSurface@@PEBV1@@Z)
- 版本问题 在
CoopGame.Build.cs中
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","PHYSICSCORE" });, 添加PHYSICSCORE项 - 换另一种获取表面类型的方式
EPhysicalSurface Surface =UGameplayStatics::GetSurfaceType(Hit);
自定义碰撞通道
1 添加碰撞通道 并 可读性宏定义

#define COLLISION_WEAPON ECC_GameTraceChannel1
- 修改枪的射线检测通道
if(GetWorld()->LineTraceSingleByChannel(OutHit, EyeLocation, TraceEnd, COLLISION_WEAPON, QueryParams))
头部暴击
- 根据物理表面类型乘上一个系数即可
if(GetWorld()->LineTraceSingleByChannel(OutHit, EyeLocation, TraceEnd, COLLISION_WEAPON, QueryParams))
{
AActor *HitActor = OutHit.GetActor();
TracerTragetLocation = OutHit.ImpactPoint;
float ActualDamage = Damage;
if(UPhysicalMaterial::DetermineSurfaceType(OutHit.PhysMaterial.Get()) == SURFACE_FLESHVULNERABLE)
ActualDamage *= VulneralbeMultiply;
UGameplayStatics::ApplyPointDamage(HitActor, ActualDamage, EyeRotator.Vector(), OutHit, GetOwner()->GetInstigatorController(), this, DamageType);
}
- 敌人蓝图调试

连续自动开火
- 修改开火绑定
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ASCharacter::StartFire);
PlayerInputComponent->BindAction("Fire", IE_Released, this, &ASCharacter::EndFire);
- 给武器添加射速
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Weapon")
float FireSpeed;
UPROPERTY()
float FireDelay;
#####################
ASWeapon::ASWeapon()
{
...
FireSpeed = 3;
}
void ASWeapon::BeginPlay()
{
...
FireDelay = 1 / FireSpeed;
}
SCharacter控制自动开火逻辑
void ASCharacter::StartFire()
{
if(HoldWeapon)
{
float FireDelay = HoldWeapon->FireDelay;
float CurDelay = FMath::Max(LastFireTime + FireDelay - GetWorld()->GetTimeSeconds(), 0.0f);
GetWorldTimerManager().SetTimer(FireTimerHandler, this, &ASCharacter::Fire, FireDelay, true, CurDelay);
}
}
void ASCharacter::Fire()
{
if(HoldWeapon)
{
HoldWeapon->Fire();
auto PC = Cast<APlayerController>(GetController()); // AController没有ClientStartCameraShake方法,需要向下强转
if(PC)
PC->ClientStartCameraShake(CameraShake);
}
LastFireTime = GetWorld()->GetTimeSeconds();
}
void ASCharacter::EndFire()
{
if(HoldWeapon)
GetWorldTimerManager().ClearTimer(FireTimerHandler);
}
Activity: 设计和实现武器功能
Ammo Comsumption (弹药消耗)
Bullet Spread (散射)
Weapon Recoil (后坐力)
Sci-fi Energy Blaster (科幻聚能武器)
四 伤与挂
自定义健康组件(包含自定义事件)
- 创建
SHealthComp类继承Actor Component
SHealthComp.h
// 自定义事件
DECLARE_DYNAMIC_MULTICAST_DELEGATE_SixParams( FOnHealthChangedSignature, USHealthComp*, HealthComp, float, CurrentHealth, float, HealthDelta, const class UDamageType*, DamageType, class AController*, InstigateBy, AActor*, DamageCauser);
---------------------------
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Health")
float DefaultHealth;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Health")
float CurrentHealth;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Owner")
class ASCharacter *MyOwner;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Health")
bool bDied;
UPROPERTY(BlueprintAssignable, Category="HealthEvents")
FOnHealthChangedSignature OnHealthChanged;
UFUNCTION()
void HandleAnyDamage(AActor* DamagedActor, float Damage, const class UDamageType* DamageType, class AController* InstigatedBy, AActor* DamageCauser);
SHealthComp.cpp
USHealthComp::USHealthComp()
{
...
DefaultHealth = 100.0f;
bDied = false;
}
void USHealthComp::BeginPlay()
{
...
CurrentHealth = DefaultHealth;
MyOwner = Cast<ASCharacter>(GetOwner());
if(MyOwner)
{
MyOwner->OnTakeAnyDamage.AddDynamic(this, &USHealthComp::HandleAnyDamage);
}
}
void USHealthComp::HandleAnyDamage(AActor* DamagedActor, float Damage, const UDamageType* DamageType,
AController* InstigatedBy, AActor* DamageCauser)
{
if(bDied) return;
float HealthDelta = FMath::Min(CurrentHealth, Damage);
CurrentHealth = FMath::Clamp(CurrentHealth - Damage, 0.0f, DefaultHealth);
if(CurrentHealth <= 0)
{
bDied = true;
// 1. 停止移动
MyOwner->GetMovementComponent()->StopMovementImmediately();
// 2. 关闭碰撞
MyOwner->GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// 3. 与控制器解绑
MyOwner->DetachFromControllerPendingDestroy();
// 4. 自动销毁
MyOwner->HoldWeapon->SetLifeSpan(3.0f);
MyOwner->SetLifeSpan(3.0f);
} else
{
OnHealthChanged.Broadcast(this, CurrentHealth, HealthDelta, DamageType, InstigatedBy, DamageCauser);
}
}
SCharacter中添加SHealthComp组件
SCharacter.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
USHealthComp* HealthComp;
SCharacter.cpp
// 血量自定义组件
HealthComp = CreateDefaultSubobject<USHealthComp>(TEXT("HealthComp"));
添加死亡
- 在
SHealthComp组件中,判断是否健康值是否死亡
bDied表示是否死亡状态,如果死亡就停止碰撞、控制器并等待销毁
SHealthComp.cpp
if(bDied) return;
float HealthDelta = FMath::Min(CurrentHealth, Damage);
CurrentHealth = FMath::Clamp(CurrentHealth - Damage, 0.0f, DefaultHealth);
if(CurrentHealth <= 0)
{
bDied = true;
// 1. 停止移动
MyOwner->GetMovementComponent()->StopMovementImmediately();
// 2. 关闭碰撞
MyOwner->GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// 3. 与控制器解绑
MyOwner->DetachFromControllerPendingDestroy();
// 4. 自动销毁
MyOwner->HoldWeapon->SetLifeSpan(3.0f);
MyOwner->SetLifeSpan(3.0f);
} else
{
OnHealthChanged.Broadcast(this, CurrentHealth, HealthDelta, DamageType, InstigatedBy, DamageCauser);
}
- 死亡动画
角色动画蓝图中添加变量Died?,并于基础姿势混合


受伤指示器
-
先创建材质
Material,命名为M_HealthIndicator

-
创建
WBP_HealthIndicator,添加Image并指定材质

-
绑定
OnHealthChanged事件,并动态设置Scalar Parameter->Alpha

tips: 测试自身受伤,可以使用Pain Causing Volume组件
Activity: 制作爆炸桶
五 游戏网络
变成网络游戏
- 设置游戏人数, 并设置开始的客户端为服务器

- 将场景中的
SCharacter都删了,然后放置两个Player Start - 创建
BP_CoopGameMode继承GameModeBase,并设置Default Pawn Class->SCharacter

World Settings->GameModeOverride->BP_CoopGameMode即可
此时人物移动可以同步
Replicate Weapon
将生成实例都由服务器负责,客户端只复制
- 将
HoldWeapon的实例化交给服务器
SCharacter.cpp
if(HasAuthority())
EquipWeapon(nullptr);
- 此时服务端会显示枪械,而客户端不显示,需要设置
SWeapon可复制
SWeapon.cpp
SetReplicates(true);
同时在蓝图中修改Replicates恢复默认值
3. 此时显示枪械正常,但是客户端无法开枪,因为客户端没有执行实例化,所以HoldWeapon为空
4. 需要设置HoldWeapon为Replicated
UPROPERTY(Replicated, VisibleAnywhere, BlueprintReadOnly, Category="Player")
ASWeapon *HoldWeapon;
---------------
void ASCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ASCharacter, HoldWeapon);
}
此时开枪同步完成
从服务器开枪 (服务端看到客户端开火)
- 客户端如果要开火,应该请求服务器开火,然后本地再开火
SWeapon.h
UFUNCTION(Server, Reliable, WithValidation)
void ServerFire();
SWeapon.cpp
void ASWeapon::ServerFire_Implementation()
{
Fire();
}
bool ASWeapon::ServerFire_Validate()
{
return true;
}
void ASWeapon::Fire()
{
if(!HasAuthority())
{
ServerFire(); // 客服端先通知服务端开火
}
// 然后才执行本地开火
...
}
只显示本地玩家UI

这样服务端能看到UI,客户端还是没有UI
让客户端开枪 (让客户端能看到服务端开火),同步枪口特效、弹道特效以及打击特效
其实已经开枪了,但是因为枪口特效、弹道特效没有同步,所以看不到
- 同步生成枪口特效和弹道特效
本质是服务端负责开枪,进行射线检测,然后客户端根据射线检测结果,去生成相应特效 - 记录服务端的射线检测结果进行同步
SWeapon.h
USTRUCT()
struct FHitScanTrace
{
GENERATED_BODY()
public:
UPROPERTY()
FVector_NetQuantize TracerTo; // 射线终点
UPROPERTY()
TEnumAsByte<EPhysicalSurface> SurfaceType; // 表面类型
UPROPERTY()
uint8 BulletCount; // 子弹数目,因为只有HitScanTrace发生改变,才会同步 这一项保证同步,每次开火++
UPROPERTY()
bool IsHit; // 是否打到物体
};
UPROPERTY(ReplicatedUsing=OnRep_HitScanTrace)
FHitScanTrace HitScanTrace;
UFUNCTION()
void OnRep_HitScanTrace();
- 在需要生成特效前,存储数据
SWeapon.cpp
void ASWeapon::PlayEffects(FVector ImpactPoint, FHitResult OutHit)
{
FVector_NetQuantize TraceTo = ImpactPoint;
bool IsHit = OutHit.GetActor() != nullptr;
EPhysicalSurface SurfaceType = SurfaceType_Default;
if(IsHit)
SurfaceType = UPhysicalMaterial::DetermineSurfaceType(OutHit.PhysMaterial.Get());
if(HasAuthority())
{
HitScanTrace.BulletCount++;
HitScanTrace.TracerTo = TraceTo;
HitScanTrace.SurfaceType = SurfaceType;
HitScanTrace.IsHit = IsHit;
}
PlayNetEffect(IsHit, SurfaceType, TraceTo);
}
- 在
HitScanTrace发生改变时,客户端回调OnRep_HitScanTrace,生成特效
void ASWeapon::OnRep_HitScanTrace()
{
PlayNetEffect(HitScanTrace.IsHit, HitScanTrace.SurfaceType, HitScanTrace.TracerTo);
}
void ASWeapon::PlayNetEffect(bool IsHit, EPhysicalSurface SurfaceType, FVector_NetQuantize TraceTo)
{
// 枪口特效
if(MuzzleEffect)
UGameplayStatics::SpawnEmitterAttached(MuzzleEffect, SkeMeshComponent, MuzzleSocketName);
// 弹道特效
if(TracerEffect)
{
FVector MuzzleLocation = SkeMeshComponent->GetSocketLocation(MuzzleSocketName);
UParticleSystemComponent *TracerComp = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), TracerEffect, MuzzleLocation);
if(TracerComp)
TracerComp->SetVectorParameter(TracerTargetName, TraceTo);
}
// 击中特效
if(IsHit)
{
// OutHit可能没有指定物理材质,通过DetermineSurfaceType会返回默认材质,不可以自己取! OutHit.PhysMaterial.Get()->SurfaceType
UParticleSystem *SelectedEffect = nullptr;
switch (SurfaceType)
{
case SURFACE_FLESHDEFAULT:
case SURFACE_FLESHVULNERABLE:
SelectedEffect = FleshImpactEffect;
// UE_LOG(LogTemp, Warning, TEXT("Flesh"));
break;
default:
SelectedEffect = DefaultImpactEffect;
// UE_LOG(LogTemp, Warning, TEXT("Default"));
break;
}
if(SelectedEffect)
{
FRotator TraceDirecton = (TraceTo - GetActorLocation()).Rotation();
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), SelectedEffect, TraceTo, TraceDirecton, FVector(0.5), true);
}
}
}
死亡同步
- 首先将伤害计算放在服务器, 同步
CurrentHealth
SHealthComp.h
UPROPERTY(Replicated, EditDefaultsOnly, BlueprintReadOnly, Category="Health")
float CurrentHealth;
SHealthComp.cpp
// 服务端才伤害绑定
if(GetOwnerRole() == ROLE_Authority)
{
if(MyOwner)
{
MyOwner->OnTakeAnyDamage.AddDynamic(this, &USHealthComp::HandleAnyDamage);
}
}
void USHealthComp::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(USHealthComp, CurrentHealth);
}
- 设置
SHealthComp复制同步
SHealthComp.cpp
SetIsReplicated(true);
SCharactor.h
- 其次将
SCharacter->bDied同步,动画蓝图中会取bDied,改变动画状态
SCharacter.h
UPROPERTY(Replicated, EditDefaultsOnly, BlueprintReadOnly, Category="Health")
bool bDied;
SCharacter.cpp
DOREPLIFETIME(ASCharacter, bDied);
爆炸桶同步
- 设置同步变量
bExplosived
SExplosiveBarrel.h
UPROPERTY(ReplicatedUsing=OnRep_Explosived, VisibleAnywhere, BlueprintReadOnly, Category="ExplosiveBarrel")
bool bExplosived;
UFUNCTION()
void OnRep_Explosived();
SExplosiveBarrel.cpp
SetReplicates(true);
SetReplicateMovement(true);
-------------
if(CurrentHealth <= 0)
{
bExplosived = true;
// 自身爆炸效果 SetReplicateMovement(true);
StaticMeshComponent->AddImpulse(FVector::UpVector * 400, NAME_None, true);
// 爆炸效果 SetReplicates(true);
RadialForceComponent->FireImpulse();
OnRep_Explosived();
}
void ASExplosiveBarrel::OnRep_Explosived()
{
// 爆炸特效
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosiveEffect, GetActorLocation(), GetActorRotation(), FVector(2));
// 修改爆炸后材质
StaticMeshComponent->SetMaterial(0, ExplosivedMaterial);
}
void ASExplosiveBarrel::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ASExplosiveBarrel, bExplosived);
}
六 游戏AI(自爆球)
准备工作
- 创建
STraceBot继承Pawn
// STraceBot.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
UStaticMeshComponent* TraceBotMeshComp;
// STraceBot.cpp
TraceBotMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TraceBotMeshComp"));
RootComponent = TraceBotMeshComp;
- 指定网格和材质
AI导航
-
拉导航体积,按
p查看导航区域


-
设置自身网格不影响
// STraceBot.cpp
TraceBotMeshComp->SetCanEverAffectNavigation(false);
- 实现MoveTo逻辑
- 蓝图

添加移动组件

- 显示导航路径

- 代码实现
- 添加依赖
CoopGame.Build.cs->NavigationSystem"" - 想要实现的目标“阶段性地给小球施加力,追踪玩家“
// STraceBot.cpp
TraceBotMeshComp->SetSimulatePhysics(true);
RequiredDistanceToTarget = 100.0f;
ForceStrength = 500.0f;
bUseVelocityChange = false;
//
void ASTraceBot::BeginPlay()
{
...
NextPathPoint = GetNextPathPoint();
}
//
void ASTraceBot::Tick(float DeltaTime)
{
...
FVector ToAddForce = NextPathPoint - GetActorLocation();
float DistanceTarget = ToAddForce.Size();
if(DistanceTarget > RequiredDistanceToTarget)
{
ToAddForce.Normalize();
ToAddForce *= ForceStrength;
TraceBotMeshComp->AddImpulse(ToAddForce, NAME_None, bUseVelocityChange);
DrawDebugDirectionalArrow(GetWorld(), GetActorLocation(), GetActorLocation() + ToAddForce, 32, FColor::Red, false, 0.0f);
} else
{
NextPathPoint = GetNextPathPoint();
DrawDebugSphere(GetWorld(), GetActorLocation(), 20, 12, FColor::Yellow, false, 0.0f);
}
}
//
FVector ASTraceBot::GetNextPathPoint()
{
ACharacter* PlayerCharacter = UGameplayStatics::GetPlayerCharacter(this, 0);
UNavigationPath* NavPath = UNavigationSystemV1::FindPathToActorSynchronously(this, GetActorLocation(), PlayerCharacter);
if(NavPath && NavPath->PathPoints.Num() > 1)
{
return NavPath->PathPoints[1];
}
return GetActorLocation();
}
与玩家互动
血量,可以被玩家攻击
- 添加血量组件, 及死亡状态
// STraceBot.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
USHealthComp* TraceBotHealthComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="TakeDamage")
bool bIsDestruct;
- 绑定血量组件事件
// Called when the game starts or when spawned
void ASTraceBot::BeginPlay()
{
...
TraceBotHealthComp->OnHealthChanged.AddDynamic(this, &ASTraceBot::GetHurt);
}
受伤闪烁
- 创建材质
Material->M_TraceBot

// STraceBot.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="TakeDamage")
UMaterialInstanceDynamic* MatInst;
- 设置到
BP_TraceBot蓝图中 - 受伤时修改
LastTimeDamageTaken参数
// STraceBot.cpp
void ASTraceBot::GetHurt(USHealthComp* HealthComp, float CurrentHealth, float HealthDelta,
const UDamageType* DamageType, AController* InstigateBy, AActor* DamageCauser)
{
...
if(MatInst == nullptr)
MatInst = TraceBotMeshComp->CreateAndSetMaterialInstanceDynamic(0);
MatInst->SetScalarParameterValue("LastTimeDamageTaken", GetWorld()->TimeSeconds);
}
血量到0自爆
- 定义爆炸所需变量
// STraceBot.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="TakeDamage")
float ExplosionRadius;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="TakeDamage")
float ExplosionDamage;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="FX")
UParticleSystem* ExplosionEffect;
- 执行爆炸逻辑
// STraceBot.cpp
void ASTraceBot::GetHurt(USHealthComp* HealthComp, float CurrentHealth, float HealthDelta,
const UDamageType* DamageType, AController* InstigateBy, AActor* DamageCauser)
{
...
if(CurrentHealth <= 0)
{
SelfDestruct();
}
}
void ASTraceBot::SelfDestruct()
{
if(bIsDestruct) return;
bIsDestruct = true;
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionEffect, GetActorLocation());
// 范围伤害
TArray<AActor*> IgnoreActors;
IgnoreActors.Add(this);
UGameplayStatics::ApplyRadialDamage(GetWorld(), ExplosionDamage, GetActorLocation(), ExplosionRadius, nullptr, IgnoreActors, this, GetInstigatorController(), true);
DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 12, FColor::Green, false, 2.0f, 0, 1.0f);
Destroy();
}
球靠近玩家,引发自爆
- 定义自爆所需变量及函数
// STraceBot.h
bool bIsStartSelfDamage;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="SelfDamage")
float SelfDamageTime;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="SelfDamage")
float SelfDamageStrength;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="SelfDamage")
float SelfDamageRadius;
// STraceBot.cpp
bIsStartSelfDamage = false;
SelfDamageTime = 0.5f;
SelfDamageStrength = 20;
SelfDamageRadius = 600;
- 定义球形网格,用于范围查询
// STraceBot.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="SelfDamage")
USphereComponent* SphereComp;
FTimerHandle TimerHandle_SelfDamage;
UFUNCTION()
void SelfDestruct();
UFUNCTION()
void StartSelfDamage();
UFUNCTION()
void DamageSelf();
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
// STraceBot.cpp
SphereComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComp"));
SphereComp->SetSphereRadius(SelfDamageRadius);
SphereComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
SphereComp->SetCollisionResponseToAllChannels(ECR_Ignore);
SphereComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
SphereComp->SetupAttachment(TraceBotMeshComp);
void ASTraceBot::SelfDestruct()
{
if(bIsDestruct) return;
GetWorldTimerManager().ClearTimer(TimerHandle_SelfDamage);
bIsDestruct = true;
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionEffect, GetActorLocation());
// 范围伤害
TArray<AActor*> IgnoreActors;
IgnoreActors.Add(this);
UGameplayStatics::ApplyRadialDamage(GetWorld(), ExplosionDamage, GetActorLocation(), ExplosionRadius, nullptr, IgnoreActors, this, GetInstigatorController(), true);
DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 12, FColor::Green, false, 2.0f, 0, 1.0f);
Destroy();
}
void ASTraceBot::StartSelfDamage()
{
bIsStartSelfDamage = true;
GetWorldTimerManager().SetTimer(TimerHandle_SelfDamage, this, &ASTraceBot::DamageSelf, SelfDamageTime, true, 0.0f);
}
void ASTraceBot::DamageSelf()
{
UGameplayStatics::ApplyDamage(this, SelfDamageStrength, GetInstigatorController(), this, nullptr);
}
void ASTraceBot::NotifyActorBeginOverlap(AActor* OtherActor)
{
if(!bIsStartSelfDamage)
{
ASCharacter* PlayerPawn = Cast<ASCharacter>(OtherActor);
if(PlayerPawn)
{
StartSelfDamage();
}
}
}
爆炸音效
WAV文件可以直接拖拽到UE中,然后新建Cue
- 定义音效片段
// STraceBot.h
UPROPERTY(EditDefaultsOnly, Category="SelfDamage")
USoundCue* SelfDestructSound;
UPROPERTY(EditDefaultsOnly, Category="SelfDamage")
USoundCue* ExplodeSound;
- 播放音效
// STraceBot.cpp
void ASTraceBot::SelfDestruct()
{
...
// 声音一次性
UGameplayStatics::PlaySoundAtLocation(this, ExplodeSound, GetActorLocation());
...
}
void ASTraceBot::StartSelfDamage()
{
...
// 声音跟随移动
UGameplayStatics::SpawnSoundAttached(SelfDestructSound, RootComponent);
...
}
- 声音衰减
-
方式一 直接在
Cue中设置

-
方式二
- 新建
Sound Attenuation


- 绑定到
Cue中

4.滚动音效
4.1 添加Audio组件,并播放滚动声音

4.2 蓝图设置 速度影响滚动声音

联机AI
现在效果是:
- 球移动正常
- 爆炸造成伤害正常
- 只在服务端 闪烁
- 伤害计算都放在服务端
// STraceBot.cpp
void ASTraceBot::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
if(!bIsStartSelfDamage && !bIsDestruct)
{
ASCharacter* PlayerPawn = Cast<ASCharacter>(OtherActor);
if(PlayerPawn)
{
if(HasAuthority())
StartSelfDamage();
}
}
}
- 同步生命值
// SHealthComp.h
UPROPERTY(ReplicatedUsing=OnRep_Health, EditDefaultsOnly, BlueprintReadOnly, Category="Health")
float CurrentHealth;
// SHealthComp.cpp
void USHealthComp::OnRep_Health(float OldHealth)
{
OnHealthChanged.Broadcast(this, CurrentHealth, CurrentHealth - OldHealth, nullptr, nullptr, nullptr);
}
void USHealthComp::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(USHealthComp, CurrentHealth);
}
- 只在服务端 爆炸特效
群体buffer
如果多个球相邻,则爆炸伤害叠加
- 范围查询
// STraceBot.h
int NearByCount;
int MaxNearByCount;
float NearByRadius;
// STraceBot.cpp
ASTraceBot::ASTraceBot()
{
...
NearByCount = 0;
MaxNearByCount = 2;
NearByRadius = 300.0f;
}
// Called when the game starts or when spawned
void ASTraceBot::BeginPlay()
{
...
if (HasAuthority())
{
NextPathPoint = GetNextPathPoint();
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this, &ASTraceBot::OnCheckNearByBots, 1.0f, true);
}
...
}
void ASTraceBot::OnCheckNearByBots()
{
FCollisionObjectQueryParams ObjectQueryParams;
ObjectQueryParams.AddObjectTypesToQuery(ECC_PhysicsBody);
ObjectQueryParams.AddObjectTypesToQuery(ECC_Pawn);
FCollisionShape CollisionShape;
CollisionShape.SetSphere(300.0f);
if(DebugTraceBotDrawing)
DrawDebugSphere(GetWorld(), GetActorLocation(), CollisionShape.GetSphereRadius(), 12, FColor::Blue, false, 1.0f);
TArray<FOverlapResult> OutOverlaps;
GetWorld()->OverlapMultiByObjectType(OutOverlaps, GetActorLocation(), FQuat::Identity, ObjectQueryParams,
CollisionShape);
for(FOverlapResult Result : OutOverlaps)
{
ASTraceBot* Bot = Cast<ASTraceBot>(Result.GetActor());
if(Bot && Bot != this)
{
NearByCount++;
}
}
NearByCount = FMath::Clamp(NearByCount, 0, MaxNearByCount);
if(MatInst == nullptr)
MatInst = TraceBotMeshComp->CreateAndSetMaterialInstanceDynamicFromMaterial(0, TraceBotMeshComp->GetMaterial(0));
float Alpha = NearByCount / (float) MaxNearByCount;
MatInst->SetScalarParameterValue("PowerLevelAlpha", Alpha);
}
- 造成伤害叠加
// STraceBot.cpp
void ASTraceBot::SelfDestruct()
{
...
if (HasAuthority())
{
// 范围伤害
TArray<AActor*> IgnoreActors;
IgnoreActors.Add(this);
float ActualDamage = ExplosionDamage * (NearByCount + 1);
UGameplayStatics::ApplyRadialDamage(this, ActualDamage, GetActorLocation(), ExplosionRadius, nullptr,
IgnoreActors, this, GetInstigatorController(), true);
if (DebugTraceBotDrawing)
DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 12, FColor::Green, false, 2.0f, 0, 1.0f);
// SetLifeSpan(2.0f);
Destroy();
}
}
浙公网安备 33010602011771号