开启增强输入系统
在UE5.1以后是默认开启的了,可以通过项目设置->输入查看。如果没有,去插件那里勾选一下。
简单使用
首先需要一个 Input Action,创建完先不用动
然后一个输入上下文 Input Mapping Context。
目前我们只需要设置一下这个Q键
首先是蓝图中的使用
- 如果使用了虚幻的服务器,增强输入的添加映射需要在主玩家身上调用。否则不需要这一步。
- IMC Test 是上面创建的输入映射上下文引用。
- 可以通过Add/ Remove Mapping Context 动态的添加和删除
- 直接在蓝图中敲 IA_Test 就能把触发事件搞出来
- 最终效果按Q键打印文本
如果是在C++里面可以这么写。实现虚函数 SetupPlayerInputComponent
void ACwlBasePlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
{
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ACwlBasePlayer::Move);
// ...
}
}
void ACwlBasePlayer::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
// ...
}
触发器
点击添加触发器可以看到若干选项,我们全部用一遍。然后通过打印事件的触发来看效果
3.1 不选触发器和Down
如果啥也不选。触发效果为,按下的时候Start,然后一直触发Trigger,松开的时候 Complete。Elapsed Second是按下按键的时候开始计时,Triggered Second 是触发Trigger后开始计时
【下移 Down】和啥也不选的表现一样,这里翻译有点诡异。区别是这里可以选一个【驱动阈值】。
我们可以看这个值默认是0.5f。通过 IsActuated 接口判断传入的值是否能够足够引起触发器的兴趣。
// InputTriggers.h
UCLASS(Abstract, Blueprintable, EditInlineNew, CollapseCategories, Config = Input, defaultconfig, configdonotcheckdefaults)
class ENHANCEDINPUT_API UInputTrigger : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Config, BlueprintReadWrite, Category = "Trigger Settings")
float ActuationThreshold = 0.5f;
// ...
/*
* Is the value passed in sufficiently large to be of interest to the trigger.
* This is a helper function that implements the most obvious (>=) interpretation of the actuation threshold.
*/
UFUNCTION(BlueprintCallable, Category="Trigger")
bool IsActuated(const FInputActionValue& ForValue) const { return ForValue.GetMagnitudeSq() >= ActuationThreshold * ActuationThreshold; }
}
然后这个传入值的计算如下。
float GetMagnitudeSq() const
{
switch (GetValueType())
{
case EInputActionValueType::Boolean:
case EInputActionValueType::Axis1D:
return Value.X * Value.X;
case EInputActionValueType::Axis2D:
return Value.SizeSquared2D();
case EInputActionValueType::Axis3D:
return Value.SizeSquared();
}
checkf(false, TEXT("Unsupported value type for input action value!"));
return 0.f;
}
3.2 Pressed 和 Released
【已按下 Pressed】Trigger事只会触发一次,其他都和上面的Down一样。
【已松开 Released】按下的时候一直触发 OnGoing,松开的时候触发 Trigger,然后是 Completed。
3.3 点按 Tap
点按释放时间阈值:时间小于这个值才算点按
受时间膨胀影响:这个好理解。调用时间膨胀接口的时候,时间的计算方式。
一次正确的点按操作应该是
Start -> On Going ... -> Trigger ... -> Completed
Start,然后一直On Going,松手的时候 Trigger, Completed
如果你按的太久了
Start,然后一直On Going,然后直接Cancel
3.4 长按 Hold
保存时间阈值:时间大于这个值才是长按
为一次性:触发一次,还是每次到达阈值都触发。Hold And Released 感觉就是默认勾选了是一次性的 Hold。
受时间膨胀影响:同上
Start -> On Going -> Cancel
按下的时候触发Start,然后一直触发 On Going,然后没到达阈值的时候松开。触发Cancel
Start -> On Going ... -> Trigger ... -> Completed
按下的时候触发Start,然后一直触发 On Going,到达阈值一直触发 Trigger,直到松开的时候触发 Completed
当勾选一次性时候,Trigger只会触发一次,然后马上 Completed
3.5 长按和松开 Hold And Released
保存时间阈值:
Start -> On Going -> Cancel
按下的时候触发Start,然后一直触发 On Going,没到阈值前松手触发Cancel
Start -> On Going-> Trigger -> Completed
按下的时候触发Start,然后一直触发 On Going,到达阈值还不放,继续On Going。松手的时候 Trigger 一次,然后 Completed 一次。
3.6 脉冲 Pulse
例如,间隔:1,触发限制:5。那就触发Start -> Trigger -> 每1s触发一次Trigger,最多5次。在这期间On Going事件一直触发。
触发限制0表示无线次。
修改器
先简单测试,按键触发是Q键
- 我们用最简单的negtive测试。按下原来是1,添加修改器后输出变成-1
- 如果Input Mapping Context 里面也填了一个 negtive,负负得正了。
如果换为2D,同时nagtive修改器。会变成这样,同时说明单按键的时候Y是没有作用了。
LogBlueprintUserMessages: [BP_TestPlayerController_C_0] X=0.000 Y=0.000
LogBlueprintUserMessages: [BP_TestPlayerController_C_0] X=-1.000 Y=-0.000
其他的。直接鼠标挪到修改器上面,找到对应类,然后搜索 ::ModifyRaw_Implementation 刚刚好是个
所以
否定(取反) Negate
勾选x, y, z 勾了就取反,很好理解
/*
* Negate
*/
FInputActionValue UInputModifierNegate::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
return CurrentValue.Get<FVector>() * FVector(bX ? -1.f : 1.f, bY ? -1.f : 1.f, bZ ? -1.f : 1.f);
}
标量 Scalar
直接乘一个缩放值好理解
FInputActionValue UInputModifierScalar::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
// Don't try and scale bools
if (ensureMsgf(CurrentValue.GetValueType() != EInputActionValueType::Boolean, TEXT("Scale modifier doesn't support boolean values.")))
{
return CurrentValue.Get<FVector>() * Scalar;
}
return CurrentValue;
}
Scale by Delta Time
/*
* Scale by Delta Time
*/
FInputActionValue UInputModifierScaleByDeltaTime::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
// Don't try and scale bools
if (ensureMsgf(CurrentValue.GetValueType() != EInputActionValueType::Boolean, TEXT("Scale By Delta Time modifier doesn't support boolean values.")))
{
return CurrentValue.Get<FVector>() * DeltaTime;
}
return CurrentValue;
};
死区
需要填入上下阈值,还有轴类型还是圆形。
写的很复杂,其实就是最大最小限制。
/*
* Dead zones
*/
FInputActionValue UInputModifierDeadZone::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
EInputActionValueType ValueType = CurrentValue.GetValueType();
if (ValueType == EInputActionValueType::Boolean)
{
return CurrentValue;
}
auto DeadZoneLambda = [this](const float AxisVal)
{
// We need to translate and scale the input to the +/- 1 range after removing the dead zone.
return FMath::Min
(1.f,
(
FMath::Max(0.f, FMath::Abs(AxisVal) - LowerThreshold)
/
(UpperThreshold - LowerThreshold)
)
) // min括号
* FMath::Sign(AxisVal);
};
FVector NewValue = CurrentValue.Get<FVector>();
switch (Type)
{
case EDeadZoneType::Axial: // 轴
NewValue.X = DeadZoneLambda(NewValue.X);
NewValue.Y = DeadZoneLambda(NewValue.Y);
NewValue.Z = DeadZoneLambda(NewValue.Z);
break;
case EDeadZoneType::Radial: // 径向 更平滑
if (ValueType == EInputActionValueType::Axis3D)
{
NewValue = NewValue.GetSafeNormal() * DeadZoneLambda(NewValue.Size());
}
else if (ValueType == EInputActionValueType::Axis2D)
{
NewValue = NewValue.GetSafeNormal2D() * DeadZoneLambda(NewValue.Size2D());
}
else
{
NewValue.X = DeadZoneLambda(NewValue.X);
}
break;
}
return NewValue;
};
平滑
FInputActionValue UInputModifierSmooth::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
uint8 SampleCount = 1;//KeyState->SampleCountAccumulator; // TODO: Need access to axis sample count accumulator here.
// TODO: This could be fired multiple times if modifiers are badly set up, breaking sample count/deltatime updates.
if(AverageValue.GetMagnitudeSq() != 0.f)
{
TotalSampleTime += DeltaTime;
Samples += SampleCount;
}
if (DeltaTime < 0.25f)
{
if (Samples > 0 && TotalSampleTime > 0.0f)
{
// this is seconds/sample
const float AxisSamplingTime = TotalSampleTime / Samples;
check(AxisSamplingTime > 0.0f);
if (CurrentValue.GetMagnitudeSq() && SampleCount > 0)
{
ZeroTime = 0.0f;
if (AverageValue.GetMagnitudeSq())
{
// this isn't the first tick with non-zero mouse movement
if (DeltaTime < AxisSamplingTime * (SampleCount + 1))
{
// smooth mouse movement so samples/tick is constant
CurrentValue *= DeltaTime / (AxisSamplingTime * SampleCount);
SampleCount = 1;
}
}
AverageValue = CurrentValue * (1.f / SampleCount);
}
else
{
// no mouse movement received
if (ZeroTime < AxisSamplingTime)
{
// zero mouse movement is possibly because less than the mouse sampling interval has passed
CurrentValue = AverageValue.ConvertToType(CurrentValue) * (DeltaTime / AxisSamplingTime);
}
else
{
ClearSmoothedAxis();
}
ZeroTime += DeltaTime; // increment length of time we've been at zero
}
}
}
else
{
// if we had an abnormally long frame, clear everything so it doesn't distort the results
ClearSmoothedAxis();
}
// TODO: FortPlayerInput clears the sample count accumulator here!
//KeyState->SampleCountAccumulator = 0;
return CurrentValue;
}
响应曲线-指数
/*
* Response curves
*/
FInputActionValue UInputModifierResponseCurveExponential::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
FVector ResponseValue = CurrentValue.Get<FVector>();
switch (CurrentValue.GetValueType())
{
case EInputActionValueType::Axis3D:
ResponseValue.Z = CurveExponent.Z != 1.f ? FMath::Sign(ResponseValue.Z) * FMath::Pow(FMath::Abs(ResponseValue.Z), CurveExponent.Z) : ResponseValue.Z;
//[[fallthrough]];
case EInputActionValueType::Axis2D:
ResponseValue.Y = CurveExponent.Y != 1.f ? FMath::Sign(ResponseValue.Y) * FMath::Pow(FMath::Abs(ResponseValue.Y), CurveExponent.Y) : ResponseValue.Y;
//[[fallthrough]];
case EInputActionValueType::Axis1D:
ResponseValue.X = CurveExponent.X != 1.f ? FMath::Sign(ResponseValue.X) * FMath::Pow(FMath::Abs(ResponseValue.X), CurveExponent.X) : ResponseValue.X;
break;
}
return ResponseValue;
};
响应曲线-用户自定义
FInputActionValue UInputModifierResponseCurveUser::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
FVector ResponseValue = CurrentValue.Get<FVector>();
switch (CurrentValue.GetValueType())
{
case EInputActionValueType::Axis3D:
ResponseValue.Z = ResponseZ ? ResponseZ->GetFloatValue(ResponseValue.Z) : 0.0f;
//[[fallthrough]];
case EInputActionValueType::Axis2D:
ResponseValue.Y = ResponseY ? ResponseY->GetFloatValue(ResponseValue.Y) : 0.0f;
//[[fallthrough]];
case EInputActionValueType::Axis1D:
case EInputActionValueType::Boolean:
ResponseValue.X = ResponseX ? ResponseX->GetFloatValue(ResponseValue.X) : 0.0f;
break;
}
return ResponseValue;
};
视野缩放
/*
* FOV
*/
FInputActionValue UInputModifierFOVScaling::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
const APlayerController* PC = PlayerInput->GetOuterAPlayerController();
if (!PC)
{
return CurrentValue;
}
const float FOVAngle = PC->PlayerCameraManager ? PC->PlayerCameraManager->GetFOVAngle() : 1.f;
float Scale = FOVScale;
switch(FOVScalingType)
{
case EFOVScalingType::Standard:
// TODO: Fortnite falls back to old style FOV scaling for mouse input. Presumably for back compat, but this needs checking.
if (PC->PlayerCameraManager)
{
// This is the proper way to scale based off FOV changes.
const float kPlayerInput_BaseFOV = 80.0f;
const float BaseHalfFOV = kPlayerInput_BaseFOV * 0.5f;
const float HalfFOV = FOVAngle * 0.5f;
const float BaseTanHalfFOV = FMath::Tan(FMath::DegreesToRadians(BaseHalfFOV));
const float TanHalfFOV = FMath::Tan(FMath::DegreesToRadians(HalfFOV));
check(BaseTanHalfFOV > 0.0f);
Scale *= (TanHalfFOV / BaseTanHalfFOV);
}
break;
case EFOVScalingType::UE4_BackCompat:
Scale *= FOVAngle;
break;
default:
checkf(false, TEXT("Unsupported FOV scaling type '%s'"), *UEnum::GetValueAsString(TEXT("EnhancedInput.EFovScalingType"), FOVScalingType));
break;
}
return CurrentValue * Scale;
}
到世界空间
左手系 右手系
/*
* ToWorldSpace axis swizzling
*/
FInputActionValue UInputModifierToWorldSpace::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
FVector Converted = CurrentValue.Get<FVector>();
switch (CurrentValue.GetValueType())
{
case EInputActionValueType::Axis3D:
// Input Device Z = World Forward (X), Device X = World Right (Y), Device Y = World Up (Z)
Converted = FVector(Converted.Z, Converted.X, Converted.Y);
break;
case EInputActionValueType::Axis2D:
// Swap axes so Input Device Y axis becomes World Forward (X), Device X becomes World Right (Y)
Swap(Converted.X, Converted.Y);
break;
case EInputActionValueType::Axis1D:
case EInputActionValueType::Boolean:
// No conversion required
break;
}
return FInputActionValue(CurrentValue.GetValueType(), Converted);
}
拌合输入轴值
/*
* Generic Axis swizzling
*/
FInputActionValue UInputModifierSwizzleAxis::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
FVector Value = CurrentValue.Get<FVector>();
switch (Order)
{
case EInputAxisSwizzle::YXZ:
Swap(Value.X, Value.Y);
break;
case EInputAxisSwizzle::ZYX:
Swap(Value.X, Value.Z);
break;
case EInputAxisSwizzle::XZY:
Swap(Value.Y, Value.Z);
break;
case EInputAxisSwizzle::YZX:
Value = FVector(Value.Y, Value.Z, Value.X);
break;
case EInputAxisSwizzle::ZXY:
Value = FVector(Value.Z, Value.X, Value.Y);
break;
}
return FInputActionValue(CurrentValue.GetValueType(), Value);
}
自定义Input Modify
蓝图版本
创建一个InputModify蓝图,然后重写接口即可
C++版本
直接抄第4点的代码即可。