Home | Site Map
Sunlight

Input Classes

Introduction

This chapter will introduce the DirectX.NET input classes, from the Sunlight.DirectX.Input namespace: the DirectInput and ActionMap classes.

This chapter introduces Marshal.Copy, the .NET interface to native callback functions and the MarshalAs attribute, and specialisation of System.Collections.CollectionBase to create type-safe collections.

Classes

DirectInput

The DirectInput class wraps an IDirectInput8 object, together with its configuration information.

__gc public class DirectInput
{
protected:
    IDirectInput8   __nogc  *m_pDI;
    DIACTION        __nogc  *m_pActions;
    DIACTIONFORMAT          m_diaf;
    Collections::ArrayList  *m_pDeviceArray;
    bool                    m_bCreated;
    
    System::Windows::Forms::Form    *m_pParentWindow;

    virtual void EnumDevices();
    virtual void BuildDIAF();
    void OnFormActivated(Object *sender, EventArgs *e);
    void OnFormDeactivated(Object *sender, EventArgs *e);
    void Create();

public:
    __delegate bool DIEnumDevicesBySemanticsCallbackDelegate(int lpddi, int lpdid, 
	int dwFlags, int dwRemaining, int pvRef);
    bool EnumDevicesBySemanticsCallback(int nddi, int ndid, int dwFlags, 
	int dwRemaining, int pvRef);
    __delegate void DIConfigureDevicesCallbackDelegate(int lpDDSTarget, int pvRef);
    void ConfigureDevicesCallback(int lpDDSTarget, int pvRef);

public:
    __gc class Device
    {
    public:
        Device(IDirectInputDevice8 __nogc *pDevice)
        {
            m_pDevice = pDevice;
            m_pDevice->AddRef();
        }
        ~Device()
        {
            m_pDevice->Release();
        }
        
        IDirectInputDevice8 __nogc *m_pDevice;
    };

public:
    DirectInput();
    ~DirectInput();

    void Acquire();
    void Unacquire();
    void Check();
    void Configure();
    void Display();
    Guid    AppID;
    Genres  Genre;
    String  *ActionMapName;
    __property System::Windows::Forms::Form *get_ParentWindow();
    __property void set_ParentWindow(System::Windows::Forms::Form *pParentWindow);
    Sunlight::DirectX::Graphics::Device *Direct3DDevice;

    __delegate void ActionEventHandler(DirectInput *sender, 
	DirectInputEventArgs *args);
    __event ActionEventHandler  *OnAction;
    ActionMap           *Actions;
};

As you can see, this class is fairly substantial.

Public Members

DirectInput exposes a handful of fields. AppID is used to uniquely identify this application to DirectInput; it should be set to a GUID that is fixed for this application. Genre defines the genre or general type of the application; the Genres enumeration is defined in ActionMap.h. ActionMapName is the name of this map, displayed by DirectInput in the configuration user interface. The ParentWindow property is used to define the focus window for DirectInput. The Direct3DDevice field is only used when a configuration UI is displayed; it gives the device on which the UI will be drawn.

The Actions field is a member of type ActionMap (described below); it defines the DirectInput action map. For more information on action maps, see the DirectInput tutorial.

Implementation Notes

DirectInput delays its major initialisation until the form is first activated. This can take several seconds, so it is useful to have some indication of the end of the task. This is again done by trapping events; this time, the parent window's standard events are used:
void DirectInput::set_ParentWindow(Form *pParentWindow)
{
    m_pParentWindow = pParentWindow;

    // Attach to the parent window Activated and Deactivated events, to 
    // acquire and unacquire.
    __hook(&System::Windows::Forms::Form::Activated, m_pParentWindow, 
	&DirectInput::OnFormActivated, this);
    __hook(&System::Windows::Forms::Form::Deactivate, m_pParentWindow, 
	&DirectInput::OnFormDeactivated, this);
    __hook(&System::Windows::Forms::Form::Closed, m_pParentWindow, 
	&DirectInput::OnFormDeactivated, this);
}
OnFormActivated merely calls Acquire, which proceeds to call Create. Create calls DirectInput8Create, to create the IDirectInput8 object. It must then build a DIACTIONFORMAT structure from the Actions member and the various parameters.
// Builds the DIACTIONFORMAT structure for this object.
void DirectInput::BuildDIAF()
{
    DIACTIONFORMAT  __pin *pDIAF = &m_diaf;

    ZeroMemory(pDIAF, sizeof(DIACTIONFORMAT));
    m_diaf.dwSize = sizeof(DIACTIONFORMAT);
    m_diaf.dwActionSize = sizeof(DIACTION);
    m_diaf.dwNumActions = Actions->Count;
    m_diaf.dwDataSize = m_diaf.dwNumActions * sizeof(DWORD);

    // Copy the GUID across.
    Marshal::Copy(AppID.ToByteArray(), 0, &pDIAF->guidActionMap, sizeof(GUID));

We use the System.Runtime.InteropServices.Marshal class again here, to provide an interface between the managed and unmanaged worlds; this time, we're calling Copy to copy the GUID.

The next task is to copy the action map entries over, for which we employ our old friend Marshal.StringToCoTaskMemxxx:

    m_pActions = new DIACTION[m_diaf.dwNumActions];
    ZeroMemory(m_pActions, sizeof(DIACTION) * m_diaf.dwNumActions);

    m_diaf.rgoAction = m_pActions;

    for (DWORD i = 0; i < m_diaf.dwNumActions; i++)
    {
        ActionMap::Entry *pEntry = Actions->Item(i);
        m_pActions[i].uAppData = pEntry->ID;
        m_pActions[i].dwSemantic = pEntry->Semantic;
#ifdef _UNICODE
        LPWSTR  p = (LPWSTR)(void *)Marshal::StringToCoTaskMemUni(pEntry->Name);
#else
        LPSTR   p = (LPSTR)(void *)Marshal::StringToCoTaskMemAnsi(pEntry->Name);
#endif
        m_pActions[i].lptszActionName = p;
    }

Next, we need to enumerate the devices by calling IDirectInput8::EnumDevicesBySemantics. This is not as easy as it sounds, since EnumDevicesBySemantics takes a pointer to a callback function. Now, .NET is perfectly able to cope with pointers to callback functions provided they are in functions .NET understands. In that case, the MarshalAs attribute can be used to declare the callback parameter, and .NET does the marshalling automatically.

This would be fantastic if we indeed did have such a declaration. Unfortunately, all we have is the standard DirectInput header file, and there's (realistically) no way to alter that. We therefore have to use a different (but related) method.

We define a global function that takes a callback parameter and calls EnumDevicesBySemantics, in a different file:

#pragma unmanaged
extern "C" HRESULT CallEnumDevicesBySemantics(IDirectInput8 *pDI,
                                           LPCTSTR lpsz, DIACTIONFORMAT *pDIAF,
                                           LPDIENUMDEVICESBYSEMANTICSCB pCallback,
                                           LPVOID pvRef, DWORD dwFlags)
{
    return pDI->EnumDevicesBySemantics(lpsz, pDIAF, pCallback, pvRef, dwFlags);
}

Now, we declare this function in DirectInput.cpp, using the MarshalAs attribute:

#pragma unmanaged
extern "C" HRESULT CallEnumDevicesBySemantics(IDirectInput8 *pDI,
    LPCTSTR lpsz, DIACTIONFORMAT *pDIAF,
    [MarshalAs(UnmanagedType::FunctionPtr)] 
	Sunlight::DirectX::Input::DirectInput::DIEnumDevicesBySemanticsCallbackDelegate *pd,
    LPVOID pvRef, DWORD dwFlags);

The DIEnumDevicesBySemanticsCallbackDelegate delegate prototype matches that of the EnumDevicesBySemantics callback prototype, except that all the arguments are integers... This peculiar arrangement is a requirement of MarshalAs (as of Beta 2 - this restriction may be lifted in later versions).

After this, the actual calling of EnumDevicesBySemantics is fairly easy, and all proceeds normally:

// Enumerates devices and builds the device array.
void DirectInput::EnumDevices()
{
    DIACTIONFORMAT  __pin *pDIAF = &m_diaf;

    HRESULT h = CallEnumDevicesBySemantics(m_pDI, NULL, pDIAF, 
        new DIEnumDevicesBySemanticsCallbackDelegate(this, 
	    &DirectInput::EnumDevicesBySemanticsCallback),
        NULL, DIEDBSFL_ATTACHEDONLY);
    if (FAILED(h))
        throw new Sunlight::DirectX::DirectXException(
	    S"IDirectInput8::EnumDevicesBySemantics", h);
}

EnumDevicesBySemanticsCallback creates a list of Device objects (simple wrappers for IDirectInputDevice8).

Acquiring and unacquiring the device is now a fairly simple process:

// Acquires the devices for use by this application.
void DirectInput::Acquire()
{
    Create();

    for (int iDevice = 0; iDevice < m_pDeviceArray->Count; iDevice++)
        static_cast<Device *>(m_pDeviceArray->Item[iDevice])->m_pDevice->Acquire();
}

Each time the application wants to poll for data (usually in the Idle event handler), it calls the Check method. This is again simple, raising an event for each of the device events:

    for (int iDevice = 0; iDevice < m_pDeviceArray->Count; iDevice++)
    {
        Device *pDev = static_cast<Device *>(m_pDeviceArray->Item[iDevice]);
        // Poll the device for data.
        pDev->m_pDevice->Poll();
    
        // Retrieve the data.
        dwObjCount = INPUT_DATA_LIMIT;
        pDev->m_pDevice->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), 
		pdidod, &dwObjCount, 0);

        if (OnAction != NULL)
        {
            for (DWORD i = 0; i < dwObjCount; i++)
                OnAction->Invoke(this, 
		    new DirectInputEventArgs(pdidod[i].uAppData, 
			(int)pdidod[i].dwData));
        }
    }

That is the majority of the input code. The remainder mostly deals with the standard DirectInput customisation interface, which uses techniques very similar to those outlined above.

ActionMap

The ActionMap class defines a collection of Entry objects, which make up a DirectInput action map.

__gc public class ActionMap : public System::Collections::CollectionBase
{
public:
    __gc class Entry
    {
    public:
        Entry();
        Entry(UINT ID, Actions Semantic, String *Name);

        UINT    ID;
        Actions Semantic;
        String  *Name;
    };

public:
    ActionMap(void);
    ~ActionMap(void);

    void Add(Entry *EntryObject);
    void Remove(int index);
    Entry *Item(int index);
};

ActionMap is a specialisation of System.Collections.CollectionBase, which provides an underlying IList implementation. This method is used instead of simply using ArrayList, to create a strongly typed collection (i.e. one that will only accept Entry objects).

Implementation Notes

ActionMap is a textbook example of how to implement a strongly typed array/list.

// Add a new entry to this action map.
void ActionMap::Add(Entry *EntryObject)
{
    List->Add(EntryObject);
}

// Remove a numbered entry from this action map.
void ActionMap::Remove(int index)
{
    // Check to see if there is an Entry at the supplied index.
    if ((index > (Count - 1)) || (index < 0))
        throw new ArgumentOutOfRangeException(S"index");

    List->RemoveAt(index);
}

// Look up a numbered entry from this action map.
ActionMap::Entry *ActionMap::Item(int index)
{
#ifdef __DEBUG
    // __try_cast will throw an exception in the unlikely event
    //  that a non-Entry Object has entered the list.
    return __try_cast<Entry *>(List->get_Item(index));
#else
    return static_cast<Entry *>(List->get_Item(index));
#endif
}

Note the use of __try_cast in debug mode, to error-check (not that it should be strictly necessary), and static_cast in release mode for speed.

In the next chapter, we'll deal with sound effects and DirectMusic.