写一些 UE C++ 开发中的一些问题和注意事项。
friend
friend class 是标准 C++ 的访问控制机制,不是 UE 特有功能。
友元可以在不把成员公开为 public 的前提下,允许某个指定类访问本类的 private 和 protected 成员。
例如:Character 和 CombatComponent。
class UCombatComponent : public UActorComponent
{
GENERATED_BODY()
public:
friend class ANinjaCharacter;
private:
ANinjaCharacter* Character;
UPROPERTY(Replicated)
AWeapon* EquippedWeapon;
};
由于 UCombatComponent 将 ANinjaCharacter 作为友元。ANinjaCharacter 就可以访问 UCombatComponent 的私有成员。
Combat->Character = this;
Combat->EquippedWeapon;
在这种情况下需要 friend 主要是需要初始化所属的 Character。
void ANinjaCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
if (Combat)
{
Combat->Character = this;
}
}
但 Character 是 UCombatComponent 的私有成员,所以需要 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);
}
}