-
Notifications
You must be signed in to change notification settings - Fork 1
I O system_(v1)
NOTE: this applies to pre-refactor branches of XVP.
XVP currently uses a custom input/output system, which has features that aren't supported by Unreal out of the box:
- (currently steering wheels only) actions rebindable by the user
- (currently steering wheels only) multi-level default binding libraries
- (steering wheels only) Force Feedback
- multiplayer support
NOTE: keyboards and gamepads are handled by Unreal Engine's input system.
XVP can use devices with no prior knowledge of them. The devices are discovered and handled by XVP device providers (see below), which tap into the host operating system directly.
In the diagram above yellow blocks denote integration points, blue blocks are XVP core classes, and green blocks are possible custom implementations you can inject at that point.
The primary integration point is the IXvpPlayerControllerIO
interface. This needs to be implemented by your AControllers
(whether they're APlayerControllers
, AAIControllers
, or something custom) that want to possess and control XVP-powered pawns
(AXvpPawn
). XVP provides two default implementations: AXvpPlayerController
and AXvpAIController
. The latter needs to be subclassed to do anything, see Using default implementation below.
The second integration point is the IXvpWheelDeviceProvider
interface and FXvpModule
. You can ignore these unless you want to implement your own device support module. See Implementing custom providers below.
To use default input settings (see Config/ExampleInput.ini) you can simply use AXvpPlayerController
as-is. You can also subclass it (with a blueprint or C++) to customize either action set configuration, or override one of the methods:
-
GetSteeringWheelConfig
should return the global steering wheel config. By default this returns the configuration stored inAXvpWorld
-
OnPreQueryInputs
is called before the action sets are updated, and can be used to override input values -
OnPostSyncInputs
is called after the action sets are updated (on the client) or after they're received (on the server), this can be used to fire custom logic off XVP actions -
ShouldSendInputs
is called on the client to determine whether theServerInput
set should be replicated to the server or not, by default they're always replicated when running in multiplayer -
SendForceFeedback
is called every frame with the generated Force Feedback effects (ornullptr
if the effect playback should be stopped). Default implementation forwards this to the wheel bound toSteering
action
AXvpPlayerController
also contains the implementation of input replication (see ShouldSendInputs
and SendInputs
RPC).
For AI vehicles you can subclass AXvpAIController
. This class doesn't implement any behaviour, it only implements all the boilerplate required for IXvpPlayerControllerIO
. In your Tick
you can simply fill ClientInput
and ServerInput
as desired.
The input and configuration is stored in FXvpInputActionSet
. Consult XvpInputActionSet.cpp
and ExampleInput.ini
included in the plugin for the current list of axes used by the default implementation.
You can change the Unreal axis names used in your subclass constructor or the blueprint, e.g.
UCLASS()
class AMyPlayerController : public AXvpPlayerController
{
GENERATED_BODY()
public:
explicit AMyPlayerController(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
/*
[/Script/Engine.InputSettings]
+AxisMappings=(AxisName="My_Throttle_Keyboard",Scale=1.000000,Key=Space)
*/
ServerInput.Throttle.UnrealBindings.AxisNames.Empty();
ServerInput.Throttle.UnrealBindings.AxisNames.Emplace("My_Throttle_Keyboard", EXvpAxisKind::Keyboard); // instead of default XVP_Throttle_Keyboard
// ...
}
// ...
};
Action configuration also includes analog emulation and deadzone settings.
NOTE: since Unreal doesn't expose information about what kind of device populated the axis, we use different axes for different device types.
TODO: blueprint screen
In normal input flow (e.g. when using AXvpPlayerController
) FXvpInputAction::Update
will be called and get the action value from the devices. To override this with your input use SetForcedNextTarget
in OnPreQueryInputs
:
virtual void OnPreQueryInputs(const float DeltaSeconds) override
{
Super::OnPreQueryInputs(DeltaSeconds);
ServerInput.Steering.SetForcedNextTarget(0.5f); // FXvpInputAction
ServerInput.GearUp.SetForcedNextTarget(true); // FXvpInputFlagAction
ServerInput.TargetGear.SetForcedNextChoice(1); // FXvpInputChoiceAction
}
Using SetValueDirectly
will not work if Update
is called -- your override will be lost. The overrides take effect when Update
is called.
Whenever Update
is not being called (AXvpAIController
or completely custom implementations), you can manipulate values directly and skip all the processing:
virtual void Tick(const float DeltaSeconds) override
{
Super::Tick(DeltaSeconds);
ServerInput.Steering.SetValueDirectly(0.5f); // FXvpInputAction
ServerInput.GearUp.SetValueDirectly(); // FXvpInputFlagAction: there is no argument because flags are consumed only once -- to set to false simply don't call this
ServerInput.TargetGear.SetValueDirectly(1); // FXvpInputChoiceAction
}
XVP expects the IXvpPlayerControllerIO
to provide two action sets to the simulation:
-
ServerInput
are actions that must be replicated from the client to the server -
ClientInput
are actions that run only on the local client
The interface contains following methods:
-
GetClientInput
: must return the currentFXvpClientInput
for this controller -
GetServerInput
: must return the currentFXvpServerInput
for this controller; this must be the same on the client and the server -
GetSteeringWheelConfig
: should return the steering wheel input; if this is incorrect, the generated Force Feedback effects or steering range will not be correct either. This is an integration point for your existing settings system, if you have one -
SendForceFeedback
: should forward the given forces to an appropriate device (nullptr
argument means "stop Force Feedback effect playback")
The best way to get an idea what's required for the custom implementation is to look at AXvpPlayerController
.
XVP will query all registered IXvpWheelDeviceProvider
at the start of the game. To register a new provider, you need to create a new Unreal module and perform the registration in StartupModule
and ShutdownModule
:
#include <XVP/XvpModule.h>
#include <XVP/IO/XvpWheelDevice.h>
struct FMyInputProviderModule : IModuleInterface
{
virtual void StartupModule() override
{
auto& Module = FModuleManager::GetModuleChecked<FXvpModule>(TEXT("XVP"));
ProviderHandle = Module.RegisterWheelDeviceProvider<FMyInputProvider>(TEXT("MyProvider"));
}
virtual void ShutdownModule() override
{
if (ProviderHandle)
{
auto& Module = FModuleManager::GetModuleChecked<FXvpModule>(TEXT("XVP"));
Module.UnregisterWheelDeviceProvider(*ProviderHandle);
}
}
virtual bool SupportsDynamicReloading() override
{
return true;
}
virtual bool IsGameModule() const override
{
return true;
}
private:
TOptional<FXvpWheelDeviceProviderHandle> ProviderHandle;
};
IMPLEMENT_GAME_MODULE(FMyInputProviderModule, MyInputProvider)
The class given to RegisterWheelDeviceProvider
must be default-constructible, must implement IXvpWheelDeviceProvider
, and will be created with MakeShared
.
The interface contains a few methods:
-
Setup
is called whenIOManager
starts up and creates the providers. This should perform any one-time setup needed by the provider. -
Shutdown
is called whenIOManager
is being torn down. This should clean up whateverSetup
prepared. -
Update
is called every frame and should returntrue
if devices were added or removed since last update. -
DiscoverDevices
should perform a device discovery, and fill the given array with pointers to currently validIXvpWheelDevice
objects managed by the provider. -
GetLabel
: should return an internal unambiguous description of the provider (this is used for logging)
struct FMyInputProvider final : IXvpWheelDeviceProvider
{
FMyInputProvider();
virtual ~FMyInputProvider() override;
virtual const TCHAR* GetLabel() override
{
return TEXT("MyProvider");
}
virtual void Setup() override
{
// initialize the underlying API, register event handlers, etc.
}
virtual void Shutdown() override
{
// tear down the underlying API, event handlers, etc.
}
virtual bool Update() override
{
// likely needs a flag that's set by some event handler registered with the underlying input API, for example:
if (bool bExpectedDevicesChanged = true; DevicesChanged.compare_exchange_strong(bExpectedDevicesChanged, false))
{
return true;
}
return false;
}
virtual void DiscoverDevices(TArray<TSharedPtr<IXvpWheelDevice>>& Devices) override
{
Devices.Emplace(MakeShared<FMyInputDevice>());
}
private:
std::atomic<bool> DevicesChanged{};
};
IXvpWheelDevice
requires several methods:
-
GetProductID
: should return the HID Product ID for the device (this is used to match devices in bindings) -
GetVendorID
: should return the HID Vendor ID for the device (this is used to match devices in bindings) -
GetLabel
: should return an internal unambiguous description of the device (this is used for logging) -
GetDisplayName
: should return the display name of the device (this is used for UI) -
GetKind
: should return the primary kind of the device (currently this should beEXvpAxisKind::Wheel
most of the time) -
GetAxis
: must return the current value of the given axis -- this should be the same asGetState()[Axis]
-
GetState
: must return the current value of all axes known to this device -
Update
: should query the current device state (if that's needed) so thatGetAxis
andGetState
return up-to-date values -
SendForceFeedback
: if the device supports Force Feedback effects (or similar), this should update the effect playback on the device
Known axis names are in XVP::IO::Axes
namespace, but the provider is not limited to using these. Axis names should generally follow the convention of <DeviceType>/<AxisName>
but are entirely freeform and matched with FName
comparisons.
For a complete example look at XVP_IO_Windows
and XVP_IO_Xbox
modules.