Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Uwaga / Notatka
Ten temat jest częścią serii samouczków pt. Tworzenie prostej gry na uniwersalną platformę Windows (UWP) z użyciem DirectX. Ten temat pod tym linkiem ustawia kontekst serii.
[ Zaktualizowano aplikacje platformy UWP w systemie Windows 10. Aby zapoznać się z artykułami dotyczącymi systemu Windows 8.x, zobacz archiwum ]
Dobra gra platformy uniwersalnej systemu Windows (UWP) obsługuje szeroką gamę interfejsów. Potencjalny gracz może mieć system Windows 10 na tablecie bez przycisków fizycznych, komputer z dołączonym kontrolerem gry lub najnowszą platformą do gier stacjonarnych z wysokowydajną myszą i klawiaturą do gier. W naszej grze sterowanie jest implementowane w klasie MoveLookController. Ta klasa agreguje wszystkie trzy typy danych wejściowych (mysz i klawiatura, dotyk i gamepad) w jeden kontroler. Rezultatem końcowym jest strzelanka pierwszoosobowa, która używa standardowych dla gatunku kontrolek ruchu i patrzenia, działających z wieloma urządzeniami.
Uwaga / Notatka
Aby uzyskać więcej informacji na temat sterowania, zobacz sterowanie wzrokiem i ruchem w grach oraz sterowanie dotykowe w grach.
Cel
W tym momencie mamy grę, która renderuje, ale nie możemy przenieść naszego gracza ani strzelać do celów. Przyjrzymy się, jak nasza gra implementuje sterowanie ruchem i patrzeniem w strzelance z perspektywy pierwszej osoby dla następujących typów wejściowych w naszej grze UWP DirectX.
- Mysz i klawiatura
- Dotyk
- Kontroler
Uwaga / Notatka
Jeśli nie pobrałeś najnowszego kodu gry tego przykładu, przejdź na przykład gry Direct3D. Ten przykład jest częścią dużej kolekcji przykładów funkcji platformy UWP. Aby uzyskać instrukcje dotyczące pobierania przykładu, zobacz Przykładowe aplikacje do programowania systemu Windows.
Typowe zachowania mechanizmów kontrolnych
Kontrolki dotykowe i kontrolki myszy/klawiatury mają bardzo podobną implementację rdzenia. W aplikacji platformy UWP wskaźnik jest po prostu punktem na ekranie. Możesz go przenieść, przesuwając mysz lub przesuwając palcem na ekranie dotykowym. W rezultacie możesz zarejestrować zestaw zdarzeń i nie martwić się, czy gracz używa myszy, czy ekranu dotykowego do poruszania się i naciskania wskaźnika.
Gdy klasa MoveLookController w przykładowej grze jest inicjowana, rejestruje się w przypadku czterech zdarzeń specyficznych dla wskaźnika i jednego zdarzenia specyficznego dla myszy:
| Zdarzenie | Opis |
|---|---|
| CoreWindow::P ointerPressed | Lewy lub prawy przycisk myszy został naciśnięty (i zatrzymany) lub powierzchnia dotykowa została dotknięcia. |
| CoreWindow::P ointerMoved | Mysz została przeniesiona lub akcja przeciągania została wykonana na powierzchni dotykowej. |
| CoreWindow::P ointerReleased | Lewy przycisk myszy został zwolniony lub obiekt kontaktujący się z powierzchnią dotykową został zniesiony. |
| CoreWindow::P ointerExited | Wskaźnik został przeniesiony z okna głównego. |
| Windows::D evices::Input::MouseMoved | Mysz przeniosła pewną odległość. Należy pamiętać, że interesuje nas tylko wartości różnicowe ruchu myszy, a nie bieżące położenie X-Y. |
Te programy obsługi zdarzeń są ustawione tak, aby uruchamiały nasłuchiwanie danych wejściowych użytkownika natychmiast po zainicjowaniu elementu MoveLookController w oknie aplikacji.
void MoveLookController::InitWindow(_In_ CoreWindow const& window)
{
ResetState();
window.PointerPressed({ this, &MoveLookController::OnPointerPressed });
window.PointerMoved({ this, &MoveLookController::OnPointerMoved });
window.PointerReleased({ this, &MoveLookController::OnPointerReleased });
window.PointerExited({ this, &MoveLookController::OnPointerExited });
...
// There is a separate handler for mouse-only relative mouse movement events.
MouseDevice::GetForCurrentView().MouseMoved({ this, &MoveLookController::OnMouseMoved });
...
}
Kompletny kod dla systemu InitWindow można zobaczyć w witrynie GitHub.
Aby określić, kiedy gra powinna nasłuchiwać niektórych danych wejściowych, klasa MoveLookController ma trzy stany specyficzne dla kontrolera, niezależnie od typu kontrolera:
| Państwo | Opis |
|---|---|
| Brak | Jest to stan zainicjowany dla kontrolera. Wszystkie dane wejściowe są ignorowane, ponieważ gra nie przewiduje żadnych danych wejściowych kontrolera. |
| CzekajNaWprowadzanie | Kontroler czeka, aż gracz potwierdzi wiadomość z gry, używając lewego kliknięcia myszy, zdarzenia dotykowego lub przycisku menu na gamepadzie. |
| Aktywny | Kontroler jest w trybie aktywnej gry. |
Stan oczekiwania na dane i wstrzymanie gry
Gra wchodzi w stan WaitForInput, gdy gra została wstrzymana. Dzieje się tak, gdy gracz przesuwa wskaźnik poza głównym oknem gry lub naciska przycisk wstrzymania (P lub przycisk Start gamepad). MoveLookController rejestruje naciśnięcie i informuje pętlę gry, gdy jest wywoływana metoda IsPauseRequested. W tym momencie jeśli IsPauseRequested zwraca true, pętla gry wywołuje WaitForPress na MoveLookController, aby przenieść kontroler do stanu WaitForInput.
Gdy gra znajduje się w stanie WaitForInput, przestaje przetwarzać prawie wszystkie zdarzenia wejściowe rozgrywki, dopóki nie powróci do stanu Active. Wyjątkiem jest przycisk wstrzymania, a naciśnięcie tego powoduje powrót gry do stanu aktywnego. Poza przyciskiem wstrzymania, aby gra wróciła do stanu Aktywne , gracz musi wybrać element menu.
Stan Aktywny
W stanie Active wystąpienie MoveLookController przetwarza zdarzenia ze wszystkich włączonych urządzeń wejściowych i interpretuje intencje gracza. W rezultacie aktualizuje szybkość i kierunek patrzenia widoku gracza oraz udostępnia zaktualizowane dane grze po wywołaniu Update z pętli gry.
Wszystkie dane wejściowe wskaźnika są śledzone w stanie Aktywny, z różnymi identyfikatorami wskaźników przypisanymi do różnych czynności wskaźnika. Po odebraniu zdarzenia PointerPressedMoveLookController uzyskuje wartość identyfikatora wskaźnika utworzoną przez okno. Identyfikator wskaźnika reprezentuje określony typ danych wejściowych. Na urządzeniu wielodotykowym, na przykład, w tym samym czasie może być kilka różnych aktywnych wejść. Identyfikatory służą do śledzenia danych wejściowych używanych przez odtwarzacz. Jeśli jedno zdarzenie znajduje się w prostokącie przesuwania ekranu dotykowego, identyfikator wskaźnika jest przypisywany do śledzenia zdarzeń wskaźnika w prostokącie przesuwania. Inne zdarzenia wskaźnika w prostokątze ognia są śledzone oddzielnie, z oddzielnym identyfikatorem wskaźnika.
Uwaga / Notatka
Dane wejściowe z myszy i prawego drążka z gamepada mają również identyfikatory, które są obsługiwane oddzielnie.
Kiedy zdarzenia wskaźnika zostały zamapowane na określoną akcję gry, nadszedł czas, aby zaktualizować dane, które obiekt MoveLookController udostępnia pętli głównej gry.
Po wywołaniu metoda Update w przykładowej grze przetwarza wejście i aktualizuje zmienne prędkości i kierunku patrzenia (m_velocity i m_lookdirection), które następnie pętla gry pobiera, wywołując publiczne metody Velocity i LookDirection.
Uwaga / Notatka
Więcej szczegółów na temat metody aktualizacji można znaleźć w dalszej części tej strony.
Pętla gry może sprawdzić, czy gracz strzela, wywołując metodę IsFiring w instancji MoveLookController. Kontrolka MoveLookController sprawdza, czy gracz nacisnął przycisk ognia na jednym z trzech typów wejściowych.
bool MoveLookController::IsFiring()
{
if (m_state == MoveLookControllerState::Active)
{
if (m_autoFire)
{
return (m_fireInUse || (m_mouseInUse && m_mouseLeftInUse) || PollingFireInUse());
}
else
{
if (m_firePressed)
{
m_firePressed = false;
return true;
}
}
}
return false;
}
Teraz przyjrzyjmy się implementacji każdego z trzech typów kontrolek w nieco bardziej szczegółowy sposób.
Dodawanie względnych kontrolek myszy
Jeśli zostanie wykryty ruch myszy, chcemy użyć tego ruchu, aby określić nowe pochylenie i odchylenie aparatu. Robimy to przez zaimplementowanie względnego sterowania myszy, w którym zajmujemy się względną odległością, na jaką mysz się przesunęła — czyli różnicą między punktem startowym a punktem zatrzymania — w przeciwieństwie do rejestrowania bezwzględnych współrzędnych x-y pikseli ruchu myszy.
W tym celu uzyskujemy zmiany w ruchu X (w poziomie) i współrzędnych Y (ruch pionowy), sprawdzając
void MoveLookController::OnMouseMoved(
_In_ MouseDevice const& /* mouseDevice */,
_In_ MouseEventArgs const& args
)
{
// Handle Mouse Input via dedicated relative movement handler.
switch (m_state)
{
case MoveLookControllerState::Active:
XMFLOAT2 mouseDelta;
mouseDelta.x = static_cast<float>(args.MouseDelta().X);
mouseDelta.y = static_cast<float>(args.MouseDelta().Y);
XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = mouseDelta.x * MoveLookConstants::RotationGain;
rotationDelta.y = mouseDelta.y * MoveLookConstants::RotationGain;
// Update our orientation based on the command.
m_pitch -= rotationDelta.y;
m_yaw += rotationDelta.x;
// Limit pitch to straight up or straight down.
float limit = XM_PI / 2.0f - 0.01f;
m_pitch = __max(-limit, m_pitch);
m_pitch = __min(+limit, m_pitch);
// Keep longitude in sane range by wrapping.
if (m_yaw > XM_PI)
{
m_yaw -= XM_PI * 2.0f;
}
else if (m_yaw < -XM_PI)
{
m_yaw += XM_PI * 2.0f;
}
break;
}
}
Dodawanie obsługi dotyku
Kontrolki dotykowe doskonale nadają się do obsługi użytkowników z tabletami. Ta gra zbiera dane wejściowe dotyku przez wydzielanie pewnych obszarów ekranu, z których każda odpowiada określonym działaniom w grze. Sterowanie dotykowe tej gry wykorzystuje trzy strefy.
Poniższe polecenia podsumowują nasze zachowanie sterowania dotykiem. Dane wejściowe użytkownika | Akcja :------- | :-------- Przenieś prostokąt | Dane wejściowe dotykowe są konwertowane na wirtualny panel sterujący, w którym ruch pionowy zostanie przetłumaczony na ruch do przodu/do tyłu, a ruch poziomy zostanie przetłumaczony na ruch w lewo/w prawo. Prostokąt ognia | Wystrzelij kulę. Dotknięcie poza prostokątem ruchu i strzału | Zmień obrót (pochylenie i odchylenie) widoku kamery.
MoveLookController sprawdza identyfikator wskaźnika, aby określić, gdzie wystąpiło zdarzenie, i wykonuje jedną z następujących akcji:
- Jeśli zdarzenie PointerMoved wystąpiło w prostokącie przenoszenia lub strzelania, zaktualizuj położenie wskaźnika kontrolera.
- Jeśli zdarzenie PointerMoved wystąpiło gdzieś w pozostałej części ekranu (określonej jako elementy sterowania wyglądem), oblicz zmianę pochylenia i odchylenia wektora kierunku widzenia.
Po zaimplementowaniu kontrolek dotykowych, prostokąty rysowane wcześniej przy użyciu Direct2D będą wskazywać graczom, gdzie znajdują się strefy ruchu, ognia i obserwacji.
Teraz przyjrzyjmy się, jak implementujemy każdą kontrolkę.
Przenoszenie i zwalnianie kontrolera
Prostokąt kontrolera przenoszenia w lewym dolnym ćwiartku ekranu jest używany jako panel kierunkowy. Przesuwając kciuk w lewo i w prawo w tym obszarze, przesuwa odtwarzacz w lewo i w prawo, podczas gdy w górę i w dół przesuwa kamerę do przodu i do tyłu. Po zakończeniu konfiguracji naciśnięcie przycisku strzału w dolnej prawej ćwiartce ekranu uruchamia sferę.
Metody SetMoveRect i SetFireRect tworzą nasze prostokąty wejściowe, biorąc dwa wektory 2D w celu określenia pozycji każdego prostokąta w lewym górnym i prawym dolnym rogu na ekranie.
Parametry są następnie przypisywane do m_fireUpperLeft i m_fireLowerRight, co pomoże nam określić, czy użytkownik dotyka któregoś z prostokątów.
m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;
Jeśli rozmiar ekranu zostanie zmieniony, te prostokąty zostaną ponownie rysowane do odpowiedniego rozmiaru.
Teraz, gdy kontrolki zostały wyłączone, nadszedł czas, aby określić, kiedy użytkownik rzeczywiście z nich korzysta. W tym celu skonfigurujemy obsługiwane zdarzenia w metodzie MoveLookController::InitWindow, gdy użytkownik naciska, przenosi lub zwalnia wskaźnik.
window.PointerPressed({ this, &MoveLookController::OnPointerPressed });
window.PointerMoved({ this, &MoveLookController::OnPointerMoved });
window.PointerReleased({ this, &MoveLookController::OnPointerReleased });
Najpierw określimy, co się stanie, gdy użytkownik najpierw naciska prostokąty przenoszenia lub uruchamiania przy użyciu metody OnPointerPressed. W tym miejscu sprawdzamy, gdzie dotykają kontrolki i czy wskaźnik znajduje się już w tym kontrolerze. Jeśli jest to pierwszy palec do dotknięcia określonej kontrolki, wykonaj następujące czynności.
- Zapisz lokalizację przyłożenia w m_moveFirstDown lub m_fireFirstDown w postaci wektora 2D.
- Przypisz identyfikator wskaźnika do m_movePointerID lub do m_firePointerID.
- Ustaw właściwą flagę InUse (m_moveInUse lub m_fireInUse) na
true, ponieważ mamy teraz aktywny wskaźnik dla tego sterowania.
PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();
auto pointerDeviceType = pointerDevice.PointerDeviceType();
XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);
...
case MoveLookControllerState::Active:
switch (pointerDeviceType)
{
case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
// Check to see if this pointer is in the move control.
if (position.x > m_moveUpperLeft.x &&
position.x < m_moveLowerRight.x &&
position.y > m_moveUpperLeft.y &&
position.y < m_moveLowerRight.y)
{
// If no pointer is in this control yet.
if (!m_moveInUse)
{
// Process a DPad touch down event.
// Save the location of the initial contact
m_moveFirstDown = position;
// Store the pointer using this control
m_movePointerID = pointerID;
// Set InUse flag to signal there is an active move pointer
m_moveInUse = true;
}
}
// Check to see if this pointer is in the fire control.
else if (position.x > m_fireUpperLeft.x &&
position.x < m_fireLowerRight.x &&
position.y > m_fireUpperLeft.y &&
position.y < m_fireLowerRight.y)
{
if (!m_fireInUse)
{
// Save the location of the initial contact
m_fireLastPoint = position;
// Store the pointer using this control
m_firePointerID = pointerID;
// Set InUse flag to signal there is an active fire pointer
m_fireInUse = true;
...
}
}
...
Teraz, gdy ustaliliśmy, czy użytkownik dotyka kontrolki ruchu lub ognia, widzimy, czy gracz wykonuje jakiekolwiek ruchy za pomocą naciśnięcia palca. Korzystając z metody MoveLookController::OnPointerMoved , sprawdzamy, jaki wskaźnik został przeniesiony, a następnie zapisujemy nową pozycję jako wektor 2D.
PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();
// convert to allow math
XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);
switch (m_state)
{
case MoveLookControllerState::Active:
// Decide which control this pointer is operating.
// Move control
if (pointerID == m_movePointerID)
{
// Save the current position.
m_movePointerPosition = position;
}
// Look control
else if (pointerID == m_lookPointerID)
{
...
}
// Fire control
else if (pointerID == m_firePointerID)
{
m_fireLastPoint = position;
}
...
Gdy użytkownik wykona gesty w obrębie elementów sterujących, zwolni wskaźnik. Korzystając z metody MoveLookController::OnPointerReleased, określamy, który wskaźnik został zwolniony i przeprowadzamy serię resetów.
Jeśli element sterujący ruchem został zwolniony, robimy, co następuje.
- Ustaw prędkość gracza na
0we wszystkich kierunkach, aby uniemożliwić im poruszanie się w grze. - Przełącz m_moveInUse na
false, ponieważ użytkownik nie dotyka już kontrolera przenoszenia. - Ustaw identyfikator wskaźnika przenoszenia na
0, ponieważ nie ma już wskaźnika w kontrolerze przenoszenia.
if (pointerID == m_movePointerID)
{
// Stop on release.
m_velocity = XMFLOAT3(0, 0, 0);
m_moveInUse = false;
m_movePointerID = 0;
}
W przypadku systemu kontroli pożaru, jeśli został zwolniony, wszystko, co robimy, to przełączenie flagi m_fireInUse na false i identyfikatora wskaźnika ognia na 0, ponieważ nie ma już wskaźnika w systemie kontroli pożaru.
else if (pointerID == m_firePointerID)
{
m_fireInUse = false;
m_firePointerID = 0;
}
Kontroler widoku
Traktujemy zdarzenia wskaźnika urządzenia dotykowego dla nieużywanych regionów ekranu jako sterowanie widokiem. Przesuwając palcem po tej strefie, zmieniasz pochylenie i obrót kamery gracza.
Jeśli zdarzenie MoveLookController::OnPointerPressed zostanie wywołane na urządzeniu dotykowym w tym regionie, a stan gry jest ustawiony na Active, przypisywany jest identyfikator wskaźnika.
// If no pointer is in this control yet.
if (!m_lookInUse)
{
// Save point for later move.
m_lookLastPoint = position;
// Store the pointer using this control.
m_lookPointerID = pointerID;
// These are for smoothing.
m_lookLastDelta.x = m_lookLastDelta.y = 0;
m_lookInUse = true;
}
W tym miejscu MoveLookController przypisuje identyfikator wskaźnika wskaźnikowi, który wyzwolił zdarzenie, do określonej zmiennej odpowiadającej regionowi widoku. W przypadku dotknięcia w regionie widoku zmienna m_lookPointerID jest ustawiona na identyfikator wskaźnika, który wyzwolił zdarzenie. Zmienna logiczna , m_lookInUse, jest również ustawiona, aby wskazać, że kontrolka nie została jeszcze zwolniona.
Teraz przyjrzyjmy się, jak przykładowa gra obsługuje zdarzenie ekranu dotykowego PointerMoved.
W metodzie MoveLookController::OnPointerMoved sprawdzamy, jaki rodzaj wskaźnika został przypisany do zdarzenia. Jeśli jest to m_lookPointerID, obliczamy zmianę pozycji wskaźnika. Następnie użyjemy tej różnicy, aby obliczyć, jak bardzo powinna się zmienić rotacja. Wreszcie jesteśmy w momencie, w którym możemy zaktualizować m_pitch i m_yaw, aby były używane w grze do zmiany rotacji gracza.
// This is the look pointer.
else if (pointerID == m_lookPointerID)
{
// Look control.
XMFLOAT2 pointerDelta;
// How far did the pointer move?
pointerDelta.x = position.x - m_lookLastPoint.x;
pointerDelta.y = position.y - m_lookLastPoint.y;
XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = pointerDelta.x * MoveLookConstants::RotationGain;
rotationDelta.y = pointerDelta.y * MoveLookConstants::RotationGain;
// Save for next time through.
m_lookLastPoint = position;
// Update our orientation based on the command.
m_pitch -= rotationDelta.y;
m_yaw += rotationDelta.x;
// Limit pitch to straight up or straight down.
float limit = XM_PI / 2.0f - 0.01f;
m_pitch = __max(-limit, m_pitch);
m_pitch = __min(+limit, m_pitch);
...
}
Ostatnim elementem, któremu się przyjrzymy, jest to, jak przykładowa gra obsługuje zdarzenie PointerReleased na ekranie dotykowym.
Po zakończeniu gestu dotykowego i usunięciu palca z ekranu, MoveLookController::OnPointerReleased zostaje uruchomiony.
Jeśli identyfikator wskaźnika, który wyzwolił zdarzenie PointerReleased, jest identyfikatorem wcześniej zarejestrowanego wskaźnika przenoszenia, MoveLookController ustawia szybkość 0, ponieważ gracz przestał dotykać obszaru wyglądu.
else if (pointerID == m_lookPointerID)
{
m_lookInUse = false;
m_lookPointerID = 0;
}
Dodawanie obsługi myszy i klawiatury
Ta gra ma następujący układ sterowania dla klawiatury i myszy.
| Dane wejściowe użytkownika | Akcja |
|---|---|
| w | Przenieś gracza do przodu |
| A | Przenieś odtwarzacz w lewo |
| S | Przenieś odtwarzacz do tyłu |
| D | Przenieś odtwarzacz w prawo |
| X | Przenieś widok w górę |
| Klawisz spacji | Przenieś widok w dół |
| P | Wstrzymaj grę |
| Ruch myszą | Zmienianie obrotu (pochylenia i odchylenia) widoku kamery |
| Lewy przycisk myszy | Wystrzelij kulę |
Aby użyć klawiatury, przykładowa gra rejestruje dwa nowe zdarzenia, CoreWindow::KeyUp i CoreWindow::KeyDown, w metodzie MoveLookController::InitWindow. Te zdarzenia obsługują naciśnięcie i zwolnienie klawisza.
window.KeyDown({ this, &MoveLookController::OnKeyDown });
window.KeyUp({ this, &MoveLookController::OnKeyUp });
Mysz jest traktowana nieco inaczej niż kontrolki dotykowe, mimo że używa wskaźnika. Aby dopasować się do układu kontrolki, MoveLookController obraca kamerę za każdym razem, gdy mysz zostanie poruszona, i strzela, gdy naciśnięty zostanie lewy przycisk myszy.
Jest to obsługiwane w metodzie OnPointerPressedMoveLookController.
W tej metodzie sprawdzamy, jaki typ urządzenia wskaźnikowego jest używany z wyliczeniem Windows::Devices::Input::PointerDeviceType.
Jeśli gra jest Active, a PointerDeviceType nie jest Touch, zakładamy, że jest to dane wejściowe myszy.
case MoveLookControllerState::Active:
switch (pointerDeviceType)
{
case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
// Behavior for touch controls
...
default:
// Behavior for mouse controls
bool rightButton = pointProperties.IsRightButtonPressed();
bool leftButton = pointProperties.IsLeftButtonPressed();
if (!m_autoFire && (!m_mouseLeftInUse && leftButton))
{
m_firePressed = true;
}
if (!m_mouseInUse)
{
m_mouseInUse = true;
m_mouseLastPoint = position;
m_mousePointerID = pointerID;
m_mouseLeftInUse = leftButton;
m_mouseRightInUse = rightButton;
// These are for smoothing.
m_lookLastDelta.x = m_lookLastDelta.y = 0;
}
break;
}
break;
Gdy gracz przestaje naciskać jeden z przycisków myszy, zostaje wyzwolone zdarzenie myszy CoreWindow::PointerReleased, które wywołuje metodę MoveLookController::OnPointerReleased, a dane wejściowe są zakończone. W tym momencie sfery przestaną strzelać, jeśli lewy przycisk myszy był naciskany i teraz został zwolniony. Ponieważ widok jest zawsze włączony, gra nadal używa tego samego wskaźnika myszy do śledzenia bieżących zdarzeń związanych z widokiem.
case MoveLookControllerState::Active:
// Touch points
if (pointerID == m_movePointerID)
{
// Stop movement
...
}
else if (pointerID == m_lookPointerID)
{
// Stop look rotation
...
}
// Fire button has been released
else if (pointerID == m_firePointerID)
{
// Stop firing
...
}
// Mouse point
else if (pointerID == m_mousePointerID)
{
bool rightButton = pointProperties.IsRightButtonPressed();
bool leftButton = pointProperties.IsLeftButtonPressed();
// Mouse no longer in use so stop firing
m_mouseInUse = false;
// Don't clear the mouse pointer ID so that Move events still result in Look changes.
// m_mousePointerID = 0;
m_mouseLeftInUse = leftButton;
m_mouseRightInUse = rightButton;
}
break;
Teraz przyjrzyjmy się ostatniego typu kontrolki, który będziemy obsługiwać: gamepady. Gamepady są obsługiwane oddzielnie od kontrolek dotyku i myszy, ponieważ nie używają obiektu wskaźnika. W związku z tym należy dodać kilka nowych procedur obsługi zdarzeń i metod.
Dodawanie obsługi gamepada
W tej grze obsługa gamepada jest dodawana przez wywołania interfejsów API Windows.Gaming.Input. Ten zestaw interfejsów API zapewnia dostęp do danych wejściowych kontrolera gry, takich jak koła wyścigowe i kije lotu.
Poniżej znajdziesz nasze kontrolki gamepadu.
| Dane wejściowe użytkownika | Akcja |
|---|---|
| Lewy kij analogowy | Przenieś odtwarzacz |
| Prawy kij analogowy | Zmienianie obrotu (pochylenia i odchylenia) widoku kamery |
| Wyzwalacz prawy | Wystrzelij kulę |
| Przycisk Start/Menu | Wstrzymywanie lub wznawianie gry |
W metodzie InitWindow dodajemy dwa nowe zdarzenia, aby określić, czy dodano lub usunięto. Te zdarzenia aktualizują właściwość m_gamepadsChanged . Jest on używany w metodzie UpdatePollingDevices, aby sprawdzić, czy lista znanych gamepadów uległa zmianie.
// Detect gamepad connection and disconnection events.
Gamepad::GamepadAdded({ this, &MoveLookController::OnGamepadAdded });
Gamepad::GamepadRemoved({ this, &MoveLookController::OnGamepadRemoved });
Uwaga / Notatka
Aplikacje platformy UWP nie mogą odbierać danych wejściowych z kontrolera gry, gdy aplikacja nie jest w centrum uwagi.
Metoda AktualizujUrządzeniaDoAnkietowania
UpdatePollingDevices metoda wystąpienia MoveLookController natychmiast sprawdza, czy gamepad jest połączony. Jeśli tak się stanie, rozpoczniemy odczytywanie jego stanu za pomocą Gamepad.GetCurrentReading. Spowoduje to zwrócenie struktury GamepadReading, co pozwala nam sprawdzić, które przyciski zostały naciśnięte lub przesunięte drążki sterujące.
Jeśli stan gry to WaitForInput, czekamy tylko na naciśnięcie przycisku Start/Menu na kontrolerze, aby można było wznowić grę.
Jeśli jest to Active, sprawdzamy dane wejściowe użytkownika i określamy, co musi się zdarzyć w grze. Jeśli na przykład użytkownik przeniósł lewy kij analogowy w określonym kierunku, pozwala to grze wiedzieć, że musimy przenieść gracza w kierunku, w którym jest przenoszony kij. Ruch kija w określonym kierunku musi być zarejestrowany jako większy niż promień martwej strefy; w przeciwnym razie nic się nie stanie. Ten zakres martwej strefy jest niezbędny, aby zapobiec "dryfowaniu", co ma miejsce, gdy kontroler rejestruje niewielkie ruchy kciuka gracza, gdy spoczywa na drążku. Bez martwych stref sterowanie może wydawać się zbyt czułe dla użytkownika.
Dane wejściowe drążka są między -1 a 1 zarówno dla osi x, jak i y. Następująca stała określa promień martwej strefy drążka.
#define THUMBSTICK_DEADZONE 0.25f
Korzystając z tej zmiennej, rozpoczniemy przetwarzanie danych wejściowych drążka sterowego. Ruch występuje z wartością [-1, -.26] lub [.26, 1] na dowolnej z osi.
Ten fragment metody UpdatePollingDevices obsługuje lewe i prawe drążki. Wartości X i Y każdego kija są sprawdzane, aby sprawdzić, czy znajdują się poza strefą martwą. Jeśli jeden lub oba elementy są, zaktualizujemy odpowiedni składnik. Na przykład, jeśli lewy drążek jest przesuwany w lewo wzdłuż osi X, dodamy -1 do składnika x wektora m_moveCommand. Ten wektor służy do agregowania wszystkich ruchów na wszystkich urządzeniach, a później będzie używany do obliczania miejsca, w którym gracz powinien się poruszać.
// Use the left thumbstick to control the eye point position
// (position of the player).
// Check if left thumbstick is outside of dead zone on x axis
if (reading.LeftThumbstickX > THUMBSTICK_DEADZONE ||
reading.LeftThumbstickX < -THUMBSTICK_DEADZONE)
{
// Get value of left thumbstick's position on x axis
float x = static_cast<float>(reading.LeftThumbstickX);
// Set the x of the move vector to 1 if the stick is being moved right.
// Set to -1 if moved left.
m_moveCommand.x -= (x > 0) ? 1 : -1;
}
// Check if left thumbstick is outside of dead zone on y axis
if (reading.LeftThumbstickY > THUMBSTICK_DEADZONE ||
reading.LeftThumbstickY < -THUMBSTICK_DEADZONE)
{
// Get value of left thumbstick's position on y axis
float y = static_cast<float>(reading.LeftThumbstickY);
// Set the y of the move vector to 1 if the stick is being moved forward.
// Set to -1 if moved backwards.
m_moveCommand.y += (y > 0) ? 1 : -1;
}
Podobnie jak ruch lewego kija, prawy kij kontroluje rotację aparatu.
Zachowanie prawego kija kciuka jest zgodne z zachowaniem ruchu myszy w naszej konfiguracji myszy i klawiatury. Jeśli drążek znajduje się poza strefą martwą, obliczamy różnicę między bieżącą pozycją wskaźnika a miejscem, w którym użytkownik próbuje teraz spojrzeć. Ta zmiana położenia wskaźnika (wskaźnikDelta) jest następnie używana do aktualizacji pochylenia i odchylenia rotacji kamery, które później są stosowane w naszej metodzie Update. Wektor wskaźnikDelta może wyglądać znajomo, ponieważ jest on również używany w metodzie MoveLookController::OnPointerMoved do śledzenia zmiany pozycji wskaźnika dla naszych danych wejściowych z myszy i dotyku.
// Use the right thumbstick to control the look at position
XMFLOAT2 pointerDelta;
// Check if right thumbstick is outside of deadzone on x axis
if (reading.RightThumbstickX > THUMBSTICK_DEADZONE ||
reading.RightThumbstickX < -THUMBSTICK_DEADZONE)
{
float x = static_cast<float>(reading.RightThumbstickX);
// Register the change in the pointer along the x axis
pointerDelta.x = x * x * x;
}
// No actionable thumbstick movement. Register no change in pointer.
else
{
pointerDelta.x = 0.0f;
}
// Check if right thumbstick is outside of deadzone on y axis
if (reading.RightThumbstickY > THUMBSTICK_DEADZONE ||
reading.RightThumbstickY < -THUMBSTICK_DEADZONE)
{
float y = static_cast<float>(reading.RightThumbstickY);
// Register the change in the pointer along the y axis
pointerDelta.y = y * y * y;
}
else
{
pointerDelta.y = 0.0f;
}
XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = pointerDelta.x * 0.08f;
rotationDelta.y = pointerDelta.y * 0.08f;
// Update our orientation based on the command.
m_pitch += rotationDelta.y;
m_yaw += rotationDelta.x;
// Limit pitch to straight up or straight down.
m_pitch = __max(-XM_PI / 2.0f, m_pitch);
m_pitch = __min(+XM_PI / 2.0f, m_pitch);
Sterowanie gry nie byłoby kompletne bez możliwości wystrzeliwania kul!
Ta metoda UpdatePollingDevices sprawdza również, czy jest naciśnięty właściwy wyzwalacz. Jeśli tak jest, nasza m_firePressed właściwość zmienia się na true, sygnalizując grze, że kule powinny zacząć strzelać.
if (reading.RightTrigger > TRIGGER_DEADZONE)
{
if (!m_autoFire && !m_gamepadTriggerInUse)
{
m_firePressed = true;
}
m_gamepadTriggerInUse = true;
}
else
{
m_gamepadTriggerInUse = false;
}
Metoda aktualizacji
Aby podsumować kwestie, przyjrzyjmy się bliżej metodzie Update. Ta metoda scala wszelkie ruchy lub rotacje, które gracz wykonał, z obsługiwanymi danymi wejściowymi, aby wygenerować wektor prędkości i zaktualizować nasze wartości pitch i yaw, do których następnie uzyskuje dostęp nasza pętla gry.
Metoda Update rozpoczyna działanie, wywołując UpdatePollingDevices, aby zaktualizować stan kontrolera. Ta metoda zbiera również wszelkie dane wejściowe z gamepadu i dodaje swoje ruchy do m_moveCommand wektora.
W naszej metodzie Update przeprowadzamy następujące kontrole danych wejściowych.
- Jeśli gracz korzysta z prostokąta kontrolera przenoszenia, określimy zmianę położenia wskaźnika i użyjemy go, aby obliczyć, czy użytkownik przeniósł wskaźnik ze strefy nieaktywnej kontrolera. Gdy znajduje się poza strefą nieaktywną, właściwość wektora m_moveCommand jest aktualizowana przez wartość wirtualnego joysticka.
- Jeśli którykolwiek z klawiszy ruchu na klawiaturze zostanie wciśnięty, wartość
1.0flub-1.0fjest dodawana do odpowiedniego składnika wektora m_moveCommand —1.0foznacza do przodu, a-1.0fdo tyłu.
Po uwzględnieniu wszystkich danych wejściowych ruchu uruchomimy wektor m_moveCommand za pomocą niektórych obliczeń, aby wygenerować nowy wektor reprezentujący kierunek gracza w odniesieniu do świata gry.
Następnie przekształcamy nasze ruchy względem świata w prędkość w tym kierunku i stosujemy je do gracza.
Na koniec zresetowaliśmy wektor m_moveCommand do wartości (0.0f, 0.0f, 0.0f), aby wszystko było gotowe do następnej ramki gry.
void MoveLookController::Update()
{
// Get any gamepad input and update state
UpdatePollingDevices();
if (m_moveInUse)
{
// Move control.
XMFLOAT2 pointerDelta;
pointerDelta.x = m_movePointerPosition.x - m_moveFirstDown.x;
pointerDelta.y = m_movePointerPosition.y - m_moveFirstDown.y;
// Figure out the command from the virtual joystick.
XMFLOAT3 commandDirection = XMFLOAT3(0.0f, 0.0f, 0.0f);
// Leave 32 pixel-wide dead spot for being still.
if (fabsf(pointerDelta.x) > 16.0f)
m_moveCommand.x -= pointerDelta.x / fabsf(pointerDelta.x);
if (fabsf(pointerDelta.y) > 16.0f)
m_moveCommand.y -= pointerDelta.y / fabsf(pointerDelta.y);
}
// Poll our state bits set by the keyboard input events.
if (m_forward)
{
m_moveCommand.y += 1.0f;
}
if (m_back)
{
m_moveCommand.y -= 1.0f;
}
if (m_left)
{
m_moveCommand.x += 1.0f;
}
if (m_right)
{
m_moveCommand.x -= 1.0f;
}
if (m_up)
{
m_moveCommand.z += 1.0f;
}
if (m_down)
{
m_moveCommand.z -= 1.0f;
}
// Make sure that 45deg cases are not faster.
if (fabsf(m_moveCommand.x) > 0.1f ||
fabsf(m_moveCommand.y) > 0.1f ||
fabsf(m_moveCommand.z) > 0.1f)
{
XMStoreFloat3(&m_moveCommand, XMVector3Normalize(XMLoadFloat3(&m_moveCommand)));
}
// Rotate command to align with our direction (world coordinates).
XMFLOAT3 wCommand;
wCommand.x = m_moveCommand.x * cosf(m_yaw) - m_moveCommand.y * sinf(m_yaw);
wCommand.y = m_moveCommand.x * sinf(m_yaw) + m_moveCommand.y * cosf(m_yaw);
wCommand.z = m_moveCommand.z;
// Scale for sensitivity adjustment.
// Our velocity is based on the command. Y is up.
m_velocity.x = -wCommand.x * MoveLookConstants::MovementGain;
m_velocity.z = wCommand.y * MoveLookConstants::MovementGain;
m_velocity.y = wCommand.z * MoveLookConstants::MovementGain;
// Clear movement input accumulator for use during next frame.
m_moveCommand = XMFLOAT3(0.0f, 0.0f, 0.0f);
}
Dalsze kroki
Teraz, gdy dodaliśmy nasze kontrolki, musimy dodać kolejną funkcję, aby utworzyć immersywną grę: dźwięk! Muzyka i efekty dźwiękowe są ważne dla każdej gry, więc następnie omówimy dodanie dźwięku.