
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.
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.
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.
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.
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).
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.