Mover的输入机制

摘要
本文介绍Mover2.0中的输入处理机制,什么是输入命令上下文,如何通过上下文传递数据到Mover系统,以及如何自定义InputProducer以操控上下文。
介绍
不像CharacterMovementComponent
(继承自PawnMovementComponent),你只需要调用AddInputVector
即可驱动角色的移动。
在Mover中,移动输入的处理变得更加灵活,但其代价是用户代码会变得更加底层且复杂。
输入命令上下文
在Mover中,将用户输入传递到Mover系统的主要方式是通过InputCmdContext(输入命令上下文),后文简称为Context。
你可以将Context简单理解为是一个MoverData
容器,可以通过蓝图/C++的API对其包含的数据进行增删改查。
这里的MoverData
指的是:继承自FMoverDataStructBase
的结构体,因此,必须通过C++新增MoverData类型。
Context可以容纳多个类型的输入参数结构体。
什么是InputProducer?
任意实现了MoverInputProducerInterface
的UObject可以视为一个InputProducer
。
MoverComponent内置一个ProduceInput函数,会在每帧调用。
1// Get latest local input prior to simulation step. Called by backend system on owner's instance (autonomous or authority).2 void ProduceInput(const int32 DeltaTimeMS, FMoverInputCmdContext* Cmd);
同时还持有一个实现了MoverInputProducerInterface
的对象的引用。上面的ProduceInput默认实现是通过下面的InputProducer对象来完成具体的输入处理逻辑。这样的设计,允许用户将输入逻辑从Mover系统中解耦。
1 /** Optional object for producing input cmds. Typically set at BeginPlay time. If not specified, defaulted input will be used. */2 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Mover, meta = (MustImplement = "/Script/Mover.MoverInputProducerInterface"))3 TObjectPtr<UObject> InputProducer;
默认情况下,MoverComponent会在Beginplay时,尝试从所属Actor或其组件树中查询实现了该接口的对象。
由于该对象是公开的,且蓝图可读写,你可以随时将任意实现了MoverInputProducerInterface
的对象赋值给这个成员即可。
尽管代码注释声称该对象是可选的,但当前Mover插件中的很多引擎代码并没有将其作为“
可选”对待。所以确保你始终提供InputProducer
用户只需要关心,如何将输入命令控制参数通过ProduceInput函数传递给InputCmdContext(输入命令上下文)。
实现MoverInputProducerInterface接口
蓝图和C++都可以实现MoverInputProducerInterface
,
1class IMoverInputProducerInterface : public IInterface2{3 GENERATED_BODY()45public:6 /** Contributes additions to the input cmd for this simulation frame. Typically this is translating accumulated user input (or AI state) into parameters that affect movement. */7 UFUNCTION(BlueprintNativeEvent)8 void ProduceInput(int32 SimTimeMs, FMoverInputCmdContext& InputCmdResult);9};
ProduceInput会在每帧执行,但是你不一定需要每帧都往Context中添加数据。
官方案例
在MoverExample插件中,MoverExamplesCharacter
这个Class实现了MoverInputProducerInterface
,并演示了如何绑定InputAction,缓存Action的值,并在OnProduceInput中,根据配置,角色当前的状态,InputAction的值,构建了针对于CharacterDefaultInputs
。你可以参考它以了解基本的用法,但请参考后文了解如何设计你的InputProducer。
设计你的InputProducer
我推荐通过Component来实现该接口,而不是通过Actor。基于组件开发能提升代码的可移植性。
使用单独的组件,实现业务的控制逻辑,通过组件的继承,从而实现不同的控制策略。
1UCLASS(ClassGroup=GMS, BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent), DisplayName="GMS Movement System Component(Mover)")2class GAME_API UMoverControlSystem : public UActorComponent, public IMoverInputProducerInterface3{4 GENERATED_BODY()5public:6 virtual void ProduceInput_Implementation(int32 SimTimeMs, FMoverInputCmdContext& InputCmdResult) override;7protected:8 // Override this function in native class to author input for the next simulation frame. Consider also calling Super method.9 UFUNCTION(BlueprintNativeEvent, Category="GMS|MovementSystem")10 FMoverInputCmdContext OnProduceInput(float DeltaMs, FMoverInputCmdContext InputCmd);11}
1void UMoverControlSystem::ProduceInput_Implementation(int32 SimTimeMs, FMoverInputCmdContext& InputCmdResult)2{3 InputCmdResult = OnProduceInput(static_cast<float>(SimTimeMs), InputCmdResult);4}56FMoverInputCmdContext UMoverControlSystem::OnProduceInput_Implementation(float DeltaMs, FMoverInputCmdContext InputCmdResult)7{8 // Your game control logic, this is where you implements your control system, feeding inputs to context.9 // 你的游戏控制逻辑,在这里实现你的控制系统,将输入传递给context.10 FCharacterDefaultInputs& CharacterInputs = InputCmdResult.InputCollection.FindOrAddMutableDataByType<FCharacterDefaultInputs>();1112 // Constructs inputs depends on your game design.13 // 根据你的游戏设计,构造合适的默认输ru参数。14 CharacterInputs.SetMoveInput(EMoveInputType::DirectionalIntent,RawMoveInputValueFromInputAction);1516 // Other kinds of inputs (abilities input, dash,double jump etc...)17 // 其他输入,技能输入,如冲刺,二段跳等。18 FYourCustomOtherInputs& OtherInputs = InputCmdResult.InputCollection.FindOrAddMutableDataByType<FYourCustomOtherInputs>();19 OtherInputs.IsDashPressed = RawDashInputValueFromInputAction;20}21
你可以通过你的组件衍生类,进行逻辑控制。
构建输入控制系统
默认角色输入
Mover插件已经提供CharacterDefaultInputs
,该结构体定义了用于控制MoverComponent
的所需的基本输入参数。
1// Data block containing all inputs that need to be authored and consumed for the default Mover character simulation2USTRUCT(BlueprintType)3struct MOVER_API FCharacterDefaultInputs : public FMoverDataStructBase4{5protected:6 UPROPERTY(BlueprintReadWrite, Category = Mover)7 EMoveInputType MoveInputType;89 // Representing the directional move input for this frame. Must be interpreted according to MoveInputType. Relative to MovementBase if set, world space otherwise. Will be truncated to match network serialization precision.10 UPROPERTY(BlueprintReadWrite, Category = Mover)11 FVector MoveInput;1213public:14 // Facing direction intent, as a normalized forward-facing direction. A zero vector indicates no intent to change facing direction. Relative to MovementBase if set, world space otherwise.15 UPROPERTY(BlueprintReadWrite, Category = Mover)16 FVector OrientationIntent;1718 // World space orientation that the controls were based on. This is commonly a player's camera rotation.19 UPROPERTY(BlueprintReadWrite, Category = Mover)20 FRotator ControlRotation;2122 // Used to force the Mover actor into a different movement mode23 UPROPERTY(BlueprintReadWrite, Category = Mover)24 FName SuggestedMovementMode;2526 // Specifies whether we are using a movement base, which will affect how move inputs are interpreted27 UPROPERTY(BlueprintReadWrite, Category = Mover)28 bool bUsingMovementBase;2930 // Optional: when moving on a base, input may be relative to this object31 UPROPERTY(BlueprintReadWrite, Category = Mover)32 TObjectPtr<UPrimitiveComponent> MovementBase;3334 // Optional: for movement bases that are skeletal meshes, this is the bone we're based on. Only valid if MovementBase is set.35 UPROPERTY(BlueprintReadWrite, Category = Mover)36 FName MovementBaseBoneName;3738 UPROPERTY(BlueprintReadWrite, Category = Mover)39 bool bIsJumpJustPressed;40 UPROPERTY(BlueprintReadWrite, Category = Mover)41 bool bIsJumpPressed;42};43
简而言之,用户可以通过动态地构建FCharacterDefaultInputs
结构体来提供输入参数,然后将其传入Context即可。

这里就是你构建专属于项目的上层控制逻辑的地方。比如根据外部信息,决定是使用轴向运动还是靶向运动等。
桥接输入系统
无论InputProducer是通过Actor,还是Component实现,它都需要知道玩家实际的输入Action值,并根据这些Action值去构建最终的输入参数。
你可以在实现了MoverInputProducerInterface
的组件(控制系统)上定义所需的InputAction值,并通过EnhancedAction事件回调,将这些值赋值到组件上,或者将其进一步封装。
其他类型的输入
默认参数能够满足角色基本的控制需求,如移动方向,跳跃请求等。但具体的项目,一般还需要各种其他输入,如Dash,Crouch,等Ability输入。
这时候就需要新增继承自FMoverDataStructBase
的结构体用于封装所需的输入参数,并将其加入Context。
在MoverExamples中,你就能看到一个示例Ability输入的案例。
1// Data block containing extended ability inputs used by MoverExamples characters2USTRUCT(BlueprintType)3struct MOVEREXAMPLES_API FMoverExampleAbilityInputs : public FMoverDataStructBase4{5 GENERATED_USTRUCT_BODY()67 UPROPERTY(BlueprintReadWrite, Category = Mover)8 bool bIsDashJustPressed = false;910 UPROPERTY(BlueprintReadWrite, Category = Mover)11 bool bIsAimPressed = false;1213 UPROPERTY(BlueprintReadWrite, Category = Mover)14 bool bIsVaultJustPressed = false;1516 UPROPERTY(BlueprintReadWrite, Category = Mover)17 bool bWantsToStartZiplining = false;1819 UPROPERTY(BlueprintReadWrite, Category = Mover)20 bool bWantsToBeCrouched = false;21};

处理输入
你有一些输入参数结构体,你也通过InputProducer将这些参数加入到了Context,接下来干什么?
接下来,你可以通过绑定MoverComponent上的OnPreSimulationTick事件,并在回调中去处理这些输入参数。并添加各种运动效果。
底层细节
在引擎底层,MoverComponent上的ProduceInput
由NetworkPrediction
插件中的NetworkPredictionWorldManager
进行调用。
它是一个世界子系统,随着世界的生命周期进行更新。我们会在以后的文章中更深入地去了解这一部分的内容。