Skip to content

UE C++ 开发基础要点解析:多人游戏开发基础

发布于  at 04:28 PM

写一些 UE C++ 开发中的一些问题和注意事项。

friend

friend class 是标准 C++ 的访问控制机制,不是 UE 特有功能。

友元可以在不把成员公开为 public 的前提下,允许某个指定类访问本类的 privateprotected 成员。

例如:CharacterCombatComponent

class UCombatComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    friend class ANinjaCharacter;

private:
    ANinjaCharacter* Character;

    UPROPERTY(Replicated)
    AWeapon* EquippedWeapon;
};

由于 UCombatComponentANinjaCharacter 作为友元。ANinjaCharacter 就可以访问 UCombatComponent 的私有成员。

Combat->Character = this;
Combat->EquippedWeapon;

在这种情况下需要 friend 主要是需要初始化所属的 Character

void ANinjaCharacter::PostInitializeComponents()
{
    Super::PostInitializeComponents();

    if (Combat)
    {
        Combat->Character = this;
    }
}

CharacterUCombatComponent 的私有成员,所以需要 friend 才可以赋值。

这种方式当然是可以的,但意味着增加了耦合。换句话说,两个类都有了对方的信息。

比如说,Character 知道了 Combat 内部有一个 Character 的成员。这个成员需要 Character 主动赋值。

耦合的问题

如果之后假设重构 UCombatComponent,假设修改成:

private:
    TArray<AWeapon*> EquippedWeapons;

    int32 ActiveWeaponIndex;

那么会导致 Character 的代码也编译失败。那么意味着,修改组件内部,角色也需要一起修改。

更好的做法是 封装,提供初始化函数替代 friend

public:
    void Initialize(ANinjaCharacter* InCharacter);

这样就不需要直接调用成员变量赋值。也给 UCombatComponent 提供了更多检查的空间。

属性复制 Replication

UE 属性复制的核心原则是:

服务器维护权威状态,属性主要从服务器复制到客户端,而不是客户端之间直接同步。

复制的内容可能包括动画。

也就说,例如捡枪,一定是客户端(玩家)发送请求给服务器,说我要捡枪,由服务器判定是否完成该动作。服务器验证通过后同时向其他玩家通知状态。

Actor 必须允许复制

bReplicates = true;

这是属性能够复制的前提。

如果是组件,则使用:

Combat->SetIsReplicated(true);

注册复制属性

复制属性通常需要三个部分:

UPROPERTY(Replicated)
AWeapon* EquippedWeapon;

这里并不是复制完整的对象,而是传递网络引用。

之后还要在 GetLifetimeReplicatedProps 中使用 DOREPLIFETIME 注册:

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

    DOREPLIFETIME(UCombatComponent, EquippedWeapon);
}

ReplicatedUsing 和 OnRep

UPROPERTY(ReplicatedUsing=OnRep_OverlappingWeapon)
AWeapon* OverlappingWeapon;

一旦角色和武器重叠,获得武器的对象(值),会调用对应的 RepNotify。

OnRep 的意义不是帮助变量复制,而是变量已经复制完成后,让客户端根据新状态执行表现层逻辑。

COND_OwnerOnly

DOREPLIFETIME_CONDITION(
    ANinjaCharacter,
    OverlappingWeapon,
    COND_OwnerOnly
);

COND_OwnerOnly 表示只复制给该角色所属的客户端。

因为拾取提示属于玩家的私有信息,玩家 A 靠近武器,只需要 A 看见提示。其他玩家不需要知道 A 当前能拾取哪一把武器。

RPC

完成捡枪的操作流程在多人游戏里大概是这样的:

客户端按下 E

EquipButtonPressed()

客户端没有 Authority(不是服务器,没资格)

ServerEquipButtonPressed()
    ↓ 网络
服务器的 ServerEquipButtonPressed_Implementation()

Combat->EquipWeapon()

修改服务器权威状态

EquippedWeapon / WeaponState / Attachment 复制到客户端

也就是我们需要一个客户端发送给服务器的请求捡枪的方法:

UFUNCTION(Server, Reliable)
void ServerEquipButtonPressed();

注意,实现的函数必须最后命名为 _Implementation

void ANinjaCharacter::ServerEquipButtonPressed_Implementation()
{
    if (Combat)
    {
        Combat->EquipWeapon(OverlappingWeapon);
    }
}

基于 HasAuthority 判断是否由服务器控制:

void ANinjaCharacter::EquipButtonPressed(const FInputActionValue& Value)
{
	if (Combat && HasAuthority())
	{
		Combat->EquipWeapon(OverlappingWeapon);
	}
	else
	{
		ServerEquipButtonPressed();
	}
}

这就意味着,Listen Server 本地玩家可以直接执行捡装备。普通客户端必须通过 Server RPC 请求,但最终捡装备行为统一由服务器完成。

服务器判定

像是否到武器区域的碰撞检测也是由服务器完成的。服务器判断角色到底有没有靠近武器,而不能是客户端不能单方面宣称碰到了武器。

void AWeapon::BeginPlay()
{
	Super::BeginPlay();

	if (HasAuthority())
	{
		AreaSphere->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
		AreaSphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
		AreaSphere->OnComponentBeginOverlap.AddDynamic(this, &AWeapon::OnSphereOverlap);
		AreaSphere->OnComponentEndOverlap.AddDynamic(this, &AWeapon::OnSphereEndOverlap);
	}

	if (PickupWidget)
	{
		PickupWidget->SetVisibility(false);
	}
}
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自小谷的随笔

上一篇
SPJ 与 UE6:Verse 语言与场景图
下一篇
Music | The Veil - Joshua Hyslop