目录

一 创建玩家

建立项目

  1. 创建空项目,导入Anim Starter Pack项目包
  2. C++ Classes->CoopGame下创建SCharacter继承Character,设置为Public
  3. SCharacter创建蓝图类BP_SCharater, 基础设置
  • 添加Skeletal Mesh
  • 设置Z-Location
  • 设置Z-Rotation: -90/270
  • 设置动画蓝图UE4ASP_HeroTPP_AnimBlueprint_C
  1. BP_SCharater拖到关卡中,设置Auto Possess PlayerPlayer 0,即可开始游戏

添加摄像机

  1. 添加组件并初始化

image

image

SpringArm的作用就是能够以人物为轴点进行旋转,如果单添加摄像机,则摄像机以自己中心点为轴心;

bug: 把组件初始化放进BeginPlay,则蓝图类中不显示; 因为生成蓝图类只会调用构造函数

  1. 蓝图类中调整SpringArm位置

输入绑定

先在Project Setting中完成输入绑定

image

移动

  1. 声明函数
    image
  2. 定义函数
    image
  3. 绑定
    image

视角

  1. 绑定
    image

蹲伏

  1. 添加蹲伏绑定
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);
  1. 设置Character可蹲伏
	GetMovementComponent()->GetNavAgentPropertiesRef().bCanCrouch = true;
bug: 设置了bCanCrouch=true,还是不能蹲伏 因为蓝图类中没有还原默认值

跳跃

  1. 绑定跳跃即可
	// 跳跃
	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"

原因:用错事件了
image

  1. 移动动画
    image

二 武器1/2

下载资源 特效和枪械

创建武器

  1. 创建SWeapon类,添加USkeletalMeshComponent组件
SkeMeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeMeshComponent"));
RootComponent = SkeMeshComponent;
  1. 创建蓝图类BP_SWeapon,并指定枪支网格
  2. SCharacter的网格体中设置WeaponSocket,然后EvnentGraph->BeginPlayer中生成枪支
    image

bug: 运行游戏直接闪退,因为没有绑定Owner,而在代码中又通过GetOwner():AActor来获取持有者,直接程序崩溃

创建弹道

  1. 从摄像头位置往前发射射线,这个位置需要修正起始位置

起点位置

从眼睛发射

一般发射点都是从眼睛的地方,即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);

应用伤害

  1. 创建敌人BP_Enemy:Actor,添加Skeletal Mesh
  2. 设置Coliision->BlockAll
  3. 打开EventGraph->Event Point Damage
    image

枪口与击中特效

定义变量

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Weapon")
UParticleSystem *MuzzleEffect;

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Weapon")
UParticleSystem *ImpactEffect;

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Weapon")
FName MuzzleSocketName;
  1. 添加击中特效
if(ImpactEffect)
	UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactEffect, OutHit.ImpactPoint, OutHit.ImpactNormal.Rotation(), FVector(0.5), true);
  1. 添加枪口特效
if(MuzzleEffect)
	UGameplayStatics::SpawnEmitterAttached(MuzzleEffect, SkeMeshComponent, MuzzleSocketName);

弹道特效

  1. 创建粒子系统,光速粒子发射器
    image
    image

可以在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"
  1. 设置TracerTargetLocation
// 初始是射线检测的终点
FVector TracerTragetLocation = TraceEnd;

// 如果命中目标,则更新为检测点
TracerTragetLocation = OutHit.ImpactPoint;
  1. 生成特效
if(TracerEffect)
{
	FVector MuzzleLocation = SkeMeshComponent->GetSocketLocation(MuzzleSocketName);
	UParticleSystemComponent *TracerComp = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), TracerEffect, MuzzleLocation);
	if(TracerComp)
		TracerComp->SetVectorParameter(TracerTargetName, TracerTragetLocation);
}

准星

  1. 创建UI/WBP_Collimator,添加Image,将Anchors->按住Ctrl+Shift设置到中心,然后将Alignment都设置为0.5
  2. 设置ScaleColor and Opacity
  3. BP_SCharacter中创建并加载到视口
    image

榴弹发射器

  1. 创建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);	
}
  1. 创建Item/BP_Projectile蓝图类,添加StaticMeshProjectileMovement组件
    StaticMesh设置显示网格;ProjectileMovement设置初始速度
  2. EventGraph中设置榴弹逻辑
    image

三 武器2/2

瞄准

  1. 定义相关变量
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;
  1. 构造函数初始化
	ZoomedFOV = 45;
	ZoomInterpSpeed = 20;
  1. 添加Input->Zoom输入绑定
Super::SetupPlayerInputComponent(PlayerInputComponent);
	...
	// 瞄准
	PlayerInputComponent->BindAction("Zoom", IE_Pressed, this, &ASCharacter::ZoomAction);
}
void ASCharacter::ZoomAction()
{
	// UE_LOG(LogTemp, Warning, TEXT("瞄准"));
	IsZooming = !IsZooming;
}
  1. 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++实现

  1. 定义变量
UPROPERTY(EditDefaultsOnly, Category="Player")
TSubclassOf<class ASWeapon> InitWeaponClass;

UPROPERTY(VisibleAnywhere, Category="Player")
ASWeapon *HoldWeapon;

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Player")
FName HoldWeaponSocketName;
bug: 程序崩溃,因为TSubclassOf需要在蓝图中初始化,代码初始化比较繁琐
  1. 构造函数中初始化
	// 武器
	HoldWeaponSocketName = "WeaponSocket";
  1. 装备初始武器
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);
	}
}
  1. 开火绑定
    添加Input->Fire绑定
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ASCharacter::Fire);

void ASCharacter::Fire()
{
	if(HoldWeapon)
		HoldWeapon->Fire();
}

开火镜头震动

  1. 创建CamerShake蓝图, Blueprint class->CamerShakeBase->DefaultCameraShakeBase, 4.27版本是这个
  2. 选择Noise模式
    image
  3. 代码中持有并调用
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);
	}
}

自定义表面

  1. Project Settings->Pyhsics->Physical Surface中添加物理表面

image

为了访问更已读,在CoopGame.h中宏定义常量

#define SURFACE_FLESHDEFAULT SurfaceType1 
#define SURFACE_FLESHVULNERABLE SurfaceType2 

注意:主代码中如果引用CoopGame.h或者CoopGame/CoopGame.h

  1. Content/Material中创建两种Physic Material, 并分别指定其Surface Type

image

image

image

  1. Physic模型身体指定物理材质
    image

image

image

  1. 根据射线检测的物理材质区分,生成不同的特效
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)

  1. 版本问题 在CoopGame.Build.cs
    PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","PHYSICSCORE" });, 添加PHYSICSCORE
  2. 换另一种获取表面类型的方式
    EPhysicalSurface Surface =UGameplayStatics::GetSurfaceType(Hit);

自定义碰撞通道

1 添加碰撞通道 并 可读性宏定义
image

#define COLLISION_WEAPON ECC_GameTraceChannel1
  1. 修改枪的射线检测通道
    if(GetWorld()->LineTraceSingleByChannel(OutHit, EyeLocation, TraceEnd, COLLISION_WEAPON, QueryParams))

头部暴击

  1. 根据物理表面类型乘上一个系数即可
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);
}
  1. 敌人蓝图调试
    image

连续自动开火

  1. 修改开火绑定
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &ASCharacter::StartFire);
PlayerInputComponent->BindAction("Fire", IE_Released, this, &ASCharacter::EndFire);
  1. 给武器添加射速
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Weapon")
float FireSpeed;

UPROPERTY()
float FireDelay;

#####################
ASWeapon::ASWeapon()
{
	...
	FireSpeed = 3;
}
void ASWeapon::BeginPlay()
{
	...
	FireDelay = 1 / FireSpeed;
}
  1. 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 (科幻聚能武器)

四 伤与挂

自定义健康组件(包含自定义事件)

  1. 创建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);
	}
}
  1. SCharacter中添加SHealthComp组件

SCharacter.h

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
	USHealthComp* HealthComp;

SCharacter.cpp

	// 血量自定义组件
	HealthComp = CreateDefaultSubobject<USHealthComp>(TEXT("HealthComp"));

添加死亡

  1. 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);
}
  1. 死亡动画

角色动画蓝图中添加变量Died?,并于基础姿势混合
image

image

受伤指示器

  1. 先创建材质Material,命名为M_HealthIndicator
    image

  2. 创建WBP_HealthIndicator,添加Image并指定材质
    image

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

tips: 测试自身受伤,可以使用Pain Causing Volume组件

Activity: 制作爆炸桶

五 游戏网络

变成网络游戏

  1. 设置游戏人数, 并设置开始的客户端为服务器
    image
  2. 将场景中的SCharacter都删了,然后放置两个Player Start
  3. 创建BP_CoopGameMode继承GameModeBase,并设置Default Pawn Class->SCharacter
    image
  4. World Settings->GameModeOverride->BP_CoopGameMode即可
    此时人物移动可以同步

Replicate Weapon

将生成实例都由服务器负责,客户端只复制

  1. HoldWeapon的实例化交给服务器
    SCharacter.cpp
if(HasAuthority())
	EquipWeapon(nullptr);
  1. 此时服务端会显示枪械,而客户端不显示,需要设置SWeapon可复制
    SWeapon.cpp
SetReplicates(true);

同时在蓝图中修改Replicates恢复默认值
3. 此时显示枪械正常,但是客户端无法开枪,因为客户端没有执行实例化,所以HoldWeapon为空
4. 需要设置HoldWeaponReplicated

UPROPERTY(Replicated, VisibleAnywhere, BlueprintReadOnly, Category="Player")
ASWeapon *HoldWeapon;

---------------

void ASCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(ASCharacter, HoldWeapon);
}

此时开枪同步完成

从服务器开枪 (服务端看到客户端开火)

  1. 客户端如果要开火,应该请求服务器开火,然后本地再开火
    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

image
这样服务端能看到UI,客户端还是没有UI

让客户端开枪 (让客户端能看到服务端开火),同步枪口特效、弹道特效以及打击特效

其实已经开枪了,但是因为枪口特效、弹道特效没有同步,所以看不到

  1. 同步生成枪口特效和弹道特效
    本质是服务端负责开枪,进行射线检测,然后客户端根据射线检测结果,去生成相应特效
  2. 记录服务端的射线检测结果进行同步
    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();
  1. 在需要生成特效前,存储数据
    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);
}
  1. 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);
		}
	}
}

死亡同步

  1. 首先将伤害计算放在服务器, 同步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);
}
  1. 设置SHealthComp复制同步
    SHealthComp.cpp
SetIsReplicated(true);

SCharactor.h

  1. 其次将SCharacter->bDied同步,动画蓝图中会取bDied,改变动画状态
    SCharacter.h
UPROPERTY(Replicated, EditDefaultsOnly, BlueprintReadOnly, Category="Health")
bool bDied;

SCharacter.cpp

DOREPLIFETIME(ASCharacter, bDied);

爆炸桶同步

  1. 设置同步变量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(自爆球)

准备工作

  1. 创建STraceBot继承Pawn
// STraceBot.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
UStaticMeshComponent* TraceBotMeshComp;

// STraceBot.cpp
TraceBotMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TraceBotMeshComp"));
RootComponent = TraceBotMeshComp;
  1. 指定网格和材质

AI导航

  1. 拉导航体积,按p查看导航区域
    image
    image

  2. 设置自身网格不影响

// STraceBot.cpp
TraceBotMeshComp->SetCanEverAffectNavigation(false);
  1. 实现MoveTo逻辑
  • 蓝图
    image
    添加移动组件
    image
  1. 显示导航路径
    image
  • 代码实现
  1. 添加依赖
    CoopGame.Build.cs->NavigationSystem""
  2. 想要实现的目标“阶段性地给小球施加力,追踪玩家“
// 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();
}

与玩家互动

血量,可以被玩家攻击

  1. 添加血量组件, 及死亡状态
// STraceBot.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
USHealthComp* TraceBotHealthComp;

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="TakeDamage")
bool bIsDestruct;
  1. 绑定血量组件事件
// Called when the game starts or when spawned
void ASTraceBot::BeginPlay()
{
	...
	TraceBotHealthComp->OnHealthChanged.AddDynamic(this, &ASTraceBot::GetHurt);
}

受伤闪烁

  1. 创建材质Material->M_TraceBot
    image
// STraceBot.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="TakeDamage")
UMaterialInstanceDynamic* MatInst;
  1. 设置到BP_TraceBot蓝图中
  2. 受伤时修改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自爆

  1. 定义爆炸所需变量
// STraceBot.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="TakeDamage")
float ExplosionRadius;

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="TakeDamage")
float ExplosionDamage;

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="FX")
UParticleSystem* ExplosionEffect;
  1. 执行爆炸逻辑
// 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();
}

球靠近玩家,引发自爆

  1. 定义自爆所需变量及函数
// 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;
  1. 定义球形网格,用于范围查询
// 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

  1. 定义音效片段
// STraceBot.h
UPROPERTY(EditDefaultsOnly, Category="SelfDamage")
USoundCue* SelfDestructSound;

UPROPERTY(EditDefaultsOnly, Category="SelfDamage")
USoundCue* ExplodeSound;
  1. 播放音效
// STraceBot.cpp
void ASTraceBot::SelfDestruct()
{
	...
	// 声音一次性
	UGameplayStatics::PlaySoundAtLocation(this, ExplodeSound, GetActorLocation());
	...
}
void ASTraceBot::StartSelfDamage()
{
	...
	// 声音跟随移动
	UGameplayStatics::SpawnSoundAttached(SelfDestructSound, RootComponent);
	...
}
  1. 声音衰减
  • 方式一 直接在Cue中设置
    image

  • 方式二

  1. 新建Sound Attenuation
    image
    image
  2. 绑定到Cue
    image

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

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

联机AI

现在效果是:

  • 球移动正常
  • 爆炸造成伤害正常
  • 只在服务端 闪烁
  1. 伤害计算都放在服务端
// STraceBot.cpp
void ASTraceBot::NotifyActorBeginOverlap(AActor* OtherActor)
{
	Super::NotifyActorBeginOverlap(OtherActor);
	
	if(!bIsStartSelfDamage && !bIsDestruct)
	{
		ASCharacter* PlayerPawn = Cast<ASCharacter>(OtherActor);
		if(PlayerPawn)
		{
			if(HasAuthority())
				StartSelfDamage();
		}
	}
}
  1. 同步生命值
// 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

如果多个球相邻,则爆炸伤害叠加

  1. 范围查询
// 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);
}
  1. 造成伤害叠加
// 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();
	}
}

增强道具

治疗道具

加速道具

教程链接