### Exercise 4: Handling Keyboard and Pointer Events ###
In this exercise, you will modify the game project from Exercise 3 to add support for handling keyboard and pointer events generated by user input to your game. The ANGLE XAML App for OpenGL ES template does not include support for keyboard and pointer events, so we will have to add code to our game.
One of the difficulties of working with keyboard and pointer events in an ANGLE app is that the events are sent to the app's UI thread or high priority input thread. However, ANGLE's OpenGL context was created on the rendering thread. If we were to send the events directly to our game code, the events would occur on the wrong thread and the app would crash. In order to work around this, we will queue the events into a thread safe queue so the events can be consumed on the rendering thread. The QueuePointerEvent() and QueueKeyEvent() methods will handle the queuing of the events in the code listed in Step 7.
-
Open Breakout.sln in the CodeLabs/Workshops/Games/Module3-ANGLE/Source/Ex4/Begin folder.
-
Select Debug x64 from the Project Configuration and Platform dropdowns.
Configuring the build target
-
Press F5 to build and run the project. The app should look like this:
Breakout App
-
Open the file OpenGLESPage.xaml.h. Add the following lines of code at the end of the class declaration in the private section.
// Setup user input void CreateInput(); // pointer handling functions void OnPointerPressed(Platform::Object^ sender, Windows::UI::Core::PointerEventArgs^ e); void OnPointerMoved(Platform::Object^ sender, Windows::UI::Core::PointerEventArgs^ e); void OnPointerReleased(Platform::Object^ sender, Windows::UI::Core::PointerEventArgs^ e); // keyboard handling functions void OnKeyPressed(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::KeyEventArgs^ args); void OnKeyReleased(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::KeyEventArgs^ args); Windows::Foundation::IAsyncAction^ mInputLoopWorker; Windows::UI::Core::CoreIndependentInputSource^ mCoreInput; float mScreenResolutionScale;
-
Open the file OpenGLESPage.xaml.cpp. Add the following using declarations at the top of the file.
using namespace Windows::System::Threading; using namespace Windows::UI::Core; using namespace Windows::UI::Xaml::Input; using namespace WinRT;
-
Modify
OpenGLESPage::OpenGLESPage()
after line 20 to include the following code.OpenGLESPage::OpenGLESPage(OpenGLES* openGLES) : mOpenGLES(openGLES), mRenderSurface(EGL_NO_SURFACE), mScreenResolutionScale(1.0f) { InitializeComponent(); Windows::UI::Core::CoreWindow^ window = Windows::UI::Xaml::Window::Current->CoreWindow; window->VisibilityChanged += ref new Windows::Foundation::TypedEventHandler<Windows::UI::Core::CoreWindow^, Windows::UI::Core::VisibilityChangedEventArgs^>(this, &OpenGLESPage::OnVisibilityChanged); this->Loaded += ref new Windows::UI::Xaml::RoutedEventHandler(this, &OpenGLESPage::OnPageLoaded); window->KeyDown += ref new TypedEventHandler<CoreWindow^, KeyEventArgs^>(this, &OpenGLESPage::OnKeyPressed); window->KeyUp += ref new TypedEventHandler<CoreWindow^, KeyEventArgs^>(this, &OpenGLESPage::OnKeyReleased); CreateInput(); }
-
Add the following methods to the end of OpenGLESPage.xaml.cpp.
void OpenGLESPage::CreateInput() { // Register our SwapChainPanel to get independent input pointer events auto workItemHandler = ref new WorkItemHandler([this](IAsyncAction ^) { // The CoreIndependentInputSource will raise pointer events for the specified device types on whichever thread it's created on. mCoreInput = swapChainPanel->CreateCoreIndependentInputSource( Windows::UI::Core::CoreInputDeviceTypes::Mouse | Windows::UI::Core::CoreInputDeviceTypes::Touch | Windows::UI::Core::CoreInputDeviceTypes::Pen ); // Register for pointer events, which will be raised on the background thread. mCoreInput->PointerPressed += ref new TypedEventHandler<Object^, PointerEventArgs^>(this, &OpenGLESPage::OnPointerPressed); mCoreInput->PointerMoved += ref new TypedEventHandler<Object^, PointerEventArgs^>(this, &OpenGLESPage::OnPointerMoved); mCoreInput->PointerReleased += ref new TypedEventHandler<Object^, PointerEventArgs^>(this, &OpenGLESPage::OnPointerReleased); // Begin processing input messages as they're delivered. mCoreInput->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessUntilQuit); }); // Run task on a dedicated high priority background thread. mInputLoopWorker = ThreadPool::RunAsync(workItemHandler, WorkItemPriority::High, WorkItemOptions::TimeSliced); } void OpenGLESPage::OnPointerPressed(Object^ sender, PointerEventArgs^ e) { mRenderer->QueuePointerEvent(PointerEventType::PointerPressed, e->CurrentPoint->Position.X * mScreenResolutionScale, e->CurrentPoint->Position.Y * mScreenResolutionScale, e->CurrentPoint->PointerId); } void OpenGLESPage::OnPointerMoved(Object^ sender, PointerEventArgs^ e) { mRenderer->QueuePointerEvent(PointerEventType::PointerMoved, e->CurrentPoint->Position.X * mScreenResolutionScale, e->CurrentPoint->Position.Y * mScreenResolutionScale, e->CurrentPoint->PointerId); } void OpenGLESPage::OnPointerReleased(Object^ sender, PointerEventArgs^ e) { mRenderer->QueuePointerEvent(PointerEventType::PointerReleased, e->CurrentPoint->Position.X * mScreenResolutionScale, e->CurrentPoint->Position.Y * mScreenResolutionScale, e->CurrentPoint->PointerId); } void OpenGLESPage::OnKeyPressed(CoreWindow^ sender, KeyEventArgs^ e) { if (!e->KeyStatus.WasKeyDown) { //log("OpenGLESPage::OnKeyPressed %d", e->VirtualKey); if (mRenderer) { mRenderer->QueueKeyEvent(KeyEventType::KeyDown, e); } } } void OpenGLESPage::OnKeyReleased(CoreWindow^ sender, KeyEventArgs^ e) { //log("OpenGLESPage::OnKeyReleased %d", e->VirtualKey); if (mRenderer) { mRenderer->QueueKeyEvent(KeyEventType::KeyUp, e); } }
-
Open the file SimpleRenderer.h and replace its contents with the following
#pragma once #include "pch.h" #include "Game.h" #include "Timer.h" #include <memory> #include "winrt/Input.h" namespace Breakout { class SimpleRenderer : public WinRT::Input { public: SimpleRenderer(); ~SimpleRenderer(); void Draw(); void UpdateWindowSize(GLsizei width, GLsizei height); private: std::shared_ptr<Game> mGame; std::unique_ptr<Timer> mTimer; GLsizei mWindowWidth; GLsizei mWindowHeight; int mDrawCount; //User input virtual void OnPointerPressed(std::shared_ptr<WinRT::PointerEvent> e); virtual void OnPointerMoved(std::shared_ptr<WinRT::PointerEvent> e); virtual void OnPointerReleased(std::shared_ptr<WinRT::PointerEvent> e); virtual void OnKeyDown(std::shared_ptr<WinRT::KeyEvent> e); virtual void OnKeyUp(std::shared_ptr<WinRT::KeyEvent> e); void SetKeyState(Windows::System::VirtualKey key, bool state); }; }
Notice that SimpleRenderer now inherits from the WinRT::Input class. This helper class implements the code needed to queue the keyboard and pointer events that arrive on the apps UI thread and make the events available to the rendering thread. You can look at this code in Framework/source/utils/winrt/Input.cpp. The code uses a thread safe concurrent_queue to handle the queuing of the events.
-
Open the file SimpleRenderer.cpp. Add the following using declaration near the top of the file:
using namespace WinRT;
-
Add a call to ProcessEvents() in the SimpleRenderer::Draw() method near line 30;
void SimpleRenderer::Draw() { if (mGame != nullptr) { float deltaTime = static_cast<float>(mTimer->getDeltaTime()); ProcessEvents(); mGame->ProcessInput(deltaTime); mGame->Update(deltaTime); mGame->Render(); } mDrawCount += 1; }
ProcessEvents() will dequeue the events and send them to our game.
-
Add the following methods to the end of SimpleRenderer.cpp
void SimpleRenderer::OnPointerPressed(std::shared_ptr<PointerEvent> e) { mGame->CursorDown(e->_x, e->_y); } void SimpleRenderer::OnPointerMoved(std::shared_ptr<PointerEvent> e) { mGame->CursorMove(e->_x, e->_y); } void SimpleRenderer::OnPointerReleased(std::shared_ptr<PointerEvent> e) { mGame->CursorUp(e->_x, e->_y); } void SimpleRenderer::OnKeyDown(std::shared_ptr<KeyEvent> e) { KeyEventArgs^ args = e->m_key.Get(); SetKeyState(args->VirtualKey, true); } void SimpleRenderer::OnKeyUp(std::shared_ptr<KeyEvent> e) { KeyEventArgs^ args = e->m_key.Get(); SetKeyState(args->VirtualKey, false); } void SimpleRenderer::SetKeyState(VirtualKey key, bool state) { GLboolean keyDown = state ? GL_TRUE : GL_FALSE; switch (key) { case VirtualKey::D: case VirtualKey::Right: mGame->Keys[GLFW_KEY_D] = keyDown; break; case VirtualKey::A: case VirtualKey::Left: mGame->Keys[GLFW_KEY_A] = keyDown; break; case VirtualKey::Space: mGame->Keys[GLFW_KEY_SPACE] = keyDown; break; } }
As you may have noticed in the above code, keyboard keys are represented by VirtualKeys. You will need to map the VirtualKey codes to the expected character codes used by your game.
-
Save your work. Press F5 to build and run your app. Your app should now respond to the following input events:
- the space bar will launch the ball
- the first click or touch event will also launch the ball
- the left and right arrows will move the paddle
- the A and D keys will also move the paddle
- mouse and touch events will move the paddle
Breakout App with Input
You have now added support for handling keyboard and pointer events.
The Breakout game originally supported only keyboard events. However, Windows 10 UWP apps running on phones or in tablet mode do not have a keyboard. We had to add support for pointer events to Game.cpp.
Take a look at Source/Game.cpp for examples of how pointer event support was added to the game.
- Continue on to Exercise 5: Adding Resources to the Game
- Return to Start