Skip to content

I O system_(v1)

Cat Plus Plus edited this page Jun 12, 2023 · 1 revision

NOTE: this applies to pre-refactor branches of XVP.

I/O system (v1)

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.

Overview

XVP_IO

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.

Using default implementation

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 in AXvpWorld
  • 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 the ServerInput 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 (or nullptr if the effect playback should be stopped). Default implementation forwards this to the wheel bound to Steering 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.

Action sets

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

Overriding values

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.

Setting values

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
}

Implementing consumer interfaces

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 current FXvpClientInput for this controller
  • GetServerInput: must return the current FXvpServerInput 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.

Implementing custom providers

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 when IOManager starts up and creates the providers. This should perform any one-time setup needed by the provider.
  • Shutdown is called when IOManager is being torn down. This should clean up whatever Setup prepared.
  • Update is called every frame and should return true if devices were added or removed since last update.
  • DiscoverDevices should perform a device discovery, and fill the given array with pointers to currently valid IXvpWheelDevice 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 be EXvpAxisKind::Wheel most of the time)
  • GetAxis: must return the current value of the given axis -- this should be the same as GetState()[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 that GetAxis and GetState 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.