Udostępnij przez


Definiowanie głównego obiektu gry

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.

Po ułożeniu podstawowej struktury przykładowej gry i zaimplementowaniu maszyny stanu obsługującej ogólne zachowania użytkownika i systemu, należy zbadać reguły i mechanikę, które zamieniają przykładową grę w grę. Przyjrzyjmy się szczegółom głównego obiektu przykładowej gry oraz sposobom tłumaczenia reguł gry na interakcje ze światem gry.

Cele i zadania

  • Dowiedz się, jak zastosować podstawowe techniki programowania w celu zaimplementowania reguł gry i mechaniki na potrzeby gry DirectX platformy UWP.

Główny obiekt gry

W przykładowej grze Simple3DGameDX, Simple3DGame jest główną klasą obiektów gry. Instancja Simple3DGame jest tworzona pośrednio przez metodę App::Load.

Poniżej przedstawiono niektóre funkcje klasy Simple3DGame.

  • Zawiera implementację logiki rozgrywki.
  • Zawiera metody, które komunikują te szczegóły.
    • Zmiany stanu gry na maszynie stanu zdefiniowanej w strukturze aplikacji.
    • Zmiany w stanie gry z aplikacji do samego obiektu gry.
    • Szczegóły dotyczące aktualizowania interfejsu użytkownika gry (nakładki i wyświetlacza HUD), animacji i fizyki (dynamicznych aspektów).

    Uwaga / Notatka

    Aktualizowanie grafiki jest obsługiwane przez klasę GameRenderer, która zawiera metody uzyskiwania i używania zasobów urządzeń graficznych używanych przez grę. Aby uzyskać więcej informacji, zobacz Rendering framework I: Wprowadzenie do renderowania.

  • Służy jako kontener dla danych, które definiują sesję, poziom lub okres istnienia gry, w zależności od sposobu definiowania gry na wysokim poziomie. W takim przypadku dane stanu gry są przez cały okres istnienia gry i są inicjowane jeden raz, gdy użytkownik uruchamia grę.

Aby wyświetlić metody i dane zdefiniowane przez tę klasę, zobacz klasa Simple3DGame poniżej.

Inicjowanie i rozpoczynanie gry

Gdy gracz rozpoczyna grę, obiekt gry musi zainicjować swój stan, utworzyć i dodać nakładkę, ustawić zmienne śledzące wydajność gracza oraz utworzyć instancje obiektów, które będą używane do budowania poziomów. W tym przykładzie jest to wykonywane w momencie utworzenia instancji GameMain w App::Load.

Obiekt gry, typu Simple3DGame, jest tworzony w konstruktorze GameMain::GameMain. Następnie jest inicjalizowany za pomocą metody Simple3DGame::Initialize w trakcie działania GameMain::ConstructInBackground jako coroutine typu "fire-and-forget", która jest wywoływana przez GameMain::GameMain.

Metoda Simple3DGame::Initialize

Przykładowa gra konfiguruje te składniki w obiekcie gry.

  • Zostanie utworzony nowy obiekt odtwarzania audio.
  • Tworzone są tablice elementów pierwotnych graficznych gry, w tym tablice dla poziomów pierwotnych, ammo i przeszkód.
  • Zostanie utworzona lokalizacja zapisywania danych o stanie gry o nazwie Gamei umieszczona w lokalizacji przechowywania ustawień danych aplikacji określonej przez ApplicationData::Current.
  • Tworzony jest czasomierz gry i początkowa mapa bitowa nakładki w grze.
  • Zostanie utworzony nowy aparat z określonym zestawem parametrów widoku i projekcji.
  • Urządzenie wejściowe (kontroler) jest ustawione na ten sam początkowy kąt pionowy i pochylenie co kamera, więc gracz ma 1-do-1 korelację między pozycją początkową sterowania a pozycją kamery.
  • Obiekt gracza jest tworzony i ustawiany na aktywny. Używamy obiektu sfery do wykrywania zbliżenia gracza do ścian i przeszkód, zapobiegając umieszczeniu kamery w pozycji, która mogłaby zakłócić wrażenie immersji.
  • Świat gry pierwotny jest tworzony.
  • Tworzone są cylindryczne przeszkody.
  • Obiekty docelowe (twarzy obiekty) są tworzone i numerowane.
  • Tworzone są kule ammo.
  • Poziomy są tworzone.
  • Załadowano wysoki wynik.
  • Zostanie załadowany dowolny wcześniej zapisany stan gry.

Gra ma teraz wystąpienia wszystkich kluczowych składników — świata, gracza, przeszkód, celów i sfer ammo. Zawiera również instancje poziomów, które odzwierciedlają konfiguracje wszystkich powyższych składników oraz ich zachowań dla każdego określonego poziomu. Teraz zobaczmy, jak gra tworzy poziomy.

Tworzenie i ładowanie poziomów gry

Większość ciężkiej pracy związanej z budową poziomów odbywa się w plikach Level[N].h/.cpp w folderze GameLevels w przykładowym rozwiązaniu. Ponieważ koncentruje się ona na bardzo konkretnej implementacji, nie będziemy ich tutaj obejmować. Ważne jest, aby kod dla każdego poziomu był uruchamiany jako oddzielny obiekt Level[N]. Jeśli chcesz rozszerzyć grę, możesz utworzyć poziom[N] obiekt, który przyjmuje przypisaną liczbę jako parametr i losowo umieszcza przeszkody i cele. Możesz również wczytać dane konfiguracyjne poziomu z pliku zasobów lub nawet z Internetu.

Definiowanie rozgrywki

W tym momencie mamy wszystkie składniki potrzebne do opracowania gry. Poziomy zostały skonstruowane w pamięci z elementów pierwotnych i są gotowe do rozpoczęcia interakcji z graczem.

Najlepsze gry reagują natychmiast na dane wejściowe gracza i przekazują natychmiastowe opinie. Dotyczy to każdego typu gry, od dynamicznych gier akcji z elementami zręcznościowymi i strzelanek pierwszoosobowych w czasie rzeczywistym do przemyślanych, turowych gier strategicznych.

Metoda Simple3DGame::RunGame

Podczas trwającego poziomu gry, gra znajduje się w stanie Dynamics.

GameMain::Update jest główną pętlą aktualizacji, która aktualizuje stan aplikacji raz na klatkę, jak pokazano poniżej. Pętla aktualizacji wywołuje metodę Simple3DGame::RunGame, aby wykonać zadania, jeśli gra znajduje się w stanie Dynamics.

// Updates the application state once per frame.
void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update();

    switch (m_updateState)
    {
    ...
    case UpdateEngineState::Dynamics:
        if (m_controller->IsPauseRequested())
        {
            ...
        }
        else
        {
            // When the player is playing, work is done by Simple3DGame::RunGame.
            GameState runState = m_game->RunGame();
            switch (runState)
            {
                ...

Simple3DGame::RunGame obsługuje zestaw danych, który definiuje bieżący stan gry dla bieżącej iteracji pętli gry.

Oto logika przepływu gry w Simple3DGame::RunGame.

  • Metoda aktualizuje czasomierz, który odlicza sekundy do momentu ukończenia poziomu, i sprawdza, czy czas poziomu wygasł. Jest to jedna z zasad gry — gdy zabraknie czasu, jeśli nie wszystkie cele zostały zastrzelone, to gra się skończyła.
  • Jeśli zabraknie czasu, metoda ustawia stan TimeExpired gry i powraca do metody Update w poprzednim kodzie.
  • Jeśli czas pozwala, kontroler ruchu i widoku jest sprawdzany w celu aktualizacji pozycji kamery; w szczególności, aktualizacji kąta widzenia normalnego wychodzącego z płaszczyzny kamery (gdzie patrzy gracz) oraz odległości, o którą ten kąt się przesunął od czasu ostatniego sprawdzenia kontrolera.
  • Kamera jest aktualizowana na podstawie nowych danych z kontrolera ruchu i wyglądu.
  • Dynamika lub animacje i zachowania obiektów w świecie gry niezależnie od kontroli gracza, są aktualizowane. W tej przykładowej grze metoda Simple3DGame::UpdateDynamics jest wywoływana, aby zaktualizować ruch wystrzelonych sfer amunicji, animację przeszkód w formie filarów i ruch celów. Aby uzyskać więcej informacji, zobacz Zaktualizuj świat gry.
  • Metoda sprawdza, czy zostały spełnione kryteria pomyślnego ukończenia poziomu. Jeśli tak, finalizuje wynik dla poziomu i sprawdza, czy jest to ostatni poziom (z 6). Jeśli jest to ostatni poziom, metoda zwraca GameState::GameComplete stanu gry; w przeciwnym razie zwraca GameState::LevelComplete stanu gry.
  • Jeśli poziom nie zostanie ukończony, metoda ustawia stan gry na GameState::Activei zwraca wartość .

Aktualizowanie świata gry

W tym przykładzie, kiedy gra jest uruchomiona, metoda Simple3DGame::UpdateDynamics jest wywoływana z metody Simple3DGame::RunGame (wywoływanej z GameMain::Update) w celu zaktualizowania obiektów renderowanych w scenie gry.

Taka pętla jak UpdateDynamics wywołuje wszelkie metody, które są używane do wprawiania świata gry w ruch, niezależnie od danych wejściowych gracza, aby stworzyć wciągające doświadczenie gry i ożywić poziom. Obejmuje to grafikę, która musi być renderowana, oraz uruchamianie pętli animacji w celu zapewnienia dynamicznego świata nawet wtedy, gdy nie ma danych wejściowych odtwarzacza. W twojej grze mogą to być drzewa kołyszące się na wietrze, fale wznoszące się wzdłuż linii brzegowych, dymiące maszyny oraz obce potwory rozciągające się i poruszające wokół. Obejmuje również interakcję między obiektami, w tym kolizjami między sferą gracza a światem lub między ammo a przeszkodami i celami.

Z wyjątkiem sytuacji, gdy gra jest specjalnie wstrzymana, pętla gry powinna kontynuować aktualizowanie świata gry, niezależnie od tego, czy jest to oparte na logice gry, algorytmach fizycznych, czy po prostu na przypadkowych zdarzeniach.

W przykładowej grze ta zasada jest nazywana dynamikąi obejmuje wzrost i opadanie przeszkód w postaci filarów, a także ruch i fizyczne zachowania kul amunicji, gdy są wystrzeliwane i w ruchu.

Metoda Simple3DGame::UpdateDynamics

Ta metoda dotyczy tych czterech zestawów obliczeń.

  • Pozycje wystrzelionych sfer ammo na świecie.
  • Animacja przeszkód filaru.
  • Przecięcie gracza i granic świata.
  • Kolizje kul amunicyjnych z przeszkodami, celami, innymi kulami amunicyjnymi i otoczeniem.

Animacja przeszkód odbywa się w pętli zdefiniowanej w plikach kodu źródłowego Animate.h/.cpp. Zachowanie ammo i wszelkie kolizje są definiowane przez uproszczone algorytmy fizyki, dostarczone w kodzie i sparametryzowane przez zestaw globalnych stałych dla świata gry, w tym właściwości grawitacji i materiału. To wszystko jest obliczane w układzie współrzędnych świata gry.

Przejrzyj przepływ

Teraz, gdy zaktualizowaliśmy wszystkie obiekty w scenie i obliczyliśmy wszelkie kolizje, musimy użyć tych informacji, aby narysować odpowiednie zmiany wizualne.

Po ukończeniu bieżącej iteracji pętli gry przez GameMain::Update, przykład natychmiast wywołuje GameRenderer::Render, aby pobrać zaktualizowane dane obiektu i wygenerować nową scenę prezentowaną graczowi, jak pokazano poniżej.

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                ...
                // Otherwise, fall through and do normal processing to perform rendering.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
                    CoreProcessEventsOption::ProcessAllIfPresent);
                // GameMain::Update calls Simple3DGame::RunGame. If game is in Dynamics
                // state, uses Simple3DGame::UpdateDynamics to update game world.
                Update();
                // Render is called immediately after the Update loop.
                m_renderer->Render();
                m_deviceResources->Present();
                m_renderNeeded = false;
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
                CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
    m_game->OnSuspending();  // Exiting due to window close, so save state.
}

Renderowanie grafiki świata gry

Zalecamy, aby grafika w grze aktualizowanej zmieniała się często, najlepiej dokładnie z taką samą częstotliwością, z jaką iteruje główna pętla gry. Podczas iteracji pętli stan świata gry jest aktualizowany, niezależnie od tego, czy gracz wprowadza dane, czy nie. Dzięki temu można bezproblemowo wyświetlać obliczone animacje i zachowania. Wyobraź sobie, że mieliśmy prostą scenę wody, która poruszała się tylko wtedy, gdy gracz nacisnął przycisk. To nie byłoby realistyczne; dobra gra wygląda gładko i płynnie przez cały czas.

Przypomnij sobie pętlę przykładowej gry, jak pokazano powyżej w GameMain::Run. Jeśli główne okno gry jest widoczne i nie jest przyciągane lub dezaktywowane, gra będzie nadal aktualizować i renderować wyniki tej aktualizacji. Metoda GameRenderer::Render, którą następnie omawiamy, renderuje reprezentację tego stanu. Dzieje się to bezpośrednio po wywołaniu GameMain::Update, które zawiera Simple3DGame::RunGame w celu aktualizacji stanów, co zostało omówione w poprzedniej sekcji.

GameRenderer::Render rysuje projekcję świata 3D, a następnie rysuje nakładkę Direct2D na niej. Po zakończeniu przedstawia końcowy łańcuch wymiany z połączonymi buforami do wyświetlania.

Uwaga / Notatka

Istnieją dwa stany nakładki Direct2D w przykładowej grze — jeden, w którym gra wyświetla nakładkę wyświetlającą informacje o grze, która zawiera mapę bitową dla menu pauzy, i jeden, w którym gra wyświetla celowniki wraz z prostokątami dla ekranu dotykowego kontrolera ruchu-i-widoku. Tekst wyniku jest wyświetlany w obu przypadkach. Aby uzyskać więcej informacji, zobacz Rendering framework I: Intro to rendering.

Metoda GameRenderer::Render

void GameRenderer::Render()
{
    bool stereoEnabled{ m_deviceResources->GetStereoState() };

    auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
    auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };

    ...
        if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
        {
            // This section is only used after the game state has been initialized and all device
            // resources needed for the game have been created and associated with the game objects.
            ...
            for (auto&& object : m_game->RenderObjects())
            {
                object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
            }
        }

        d3dContext->BeginEventInt(L"D2D BeginDraw", 1);
        d2dContext->BeginDraw();

        // To handle the swapchain being pre-rotated, set the D2D transformation to include it.
        d2dContext->SetTransform(m_deviceResources->GetOrientationTransform2D());

        if (m_game != nullptr && m_gameResourcesLoaded)
        {
            // This is only used after the game state has been initialized.
            m_gameHud.Render(m_game);
        }

        if (m_gameInfoOverlay.Visible())
        {
            d2dContext->DrawBitmap(
                m_gameInfoOverlay.Bitmap(),
                m_gameInfoOverlayRect
                );
        }
        ...
    }
}

Klasa Simple3DGame

Są to metody i składowe danych zdefiniowane przez klasę Simple3DGame.

Funkcje składowe

Publiczne funkcje składowe zdefiniowane przez Simple3DGame obejmują poniższe.

  • Zainicjuj. Ustawia wartości początkowe zmiennych globalnych i inicjuje obiekty gry. Opisano to w sekcji Zainicjuj i rozpocznij grę.
  • LoadGame. Inicjuje nowy poziom i rozpoczyna ładowanie go.
  • LoadLevelAsync. Korutyna, która inicjuje poziom, a następnie wywołuje inną korutynę na rendererze, aby załadować zasoby poziomu specyficzne dla urządzenia. Ta metoda jest uruchamiana w osobnym wątku; W rezultacie tylko metody ID3D11Device (w przeciwieństwie do metod ID3D11DeviceContext) mogą być wywoływane z tego wątku. Wszystkie metody kontekstu urządzenia są wywoływane w metodzie FinalizeLoadLevel. Jeśli dopiero zaczynasz przygodę z programowaniem asynchronicznym, zobacz w sekcji współbieżność i operacje asynchroniczne za pomocą języka C++/WinRT.
  • FinalizujLoadLevel. Wykonuje wszelkie prace związane z ładowaniem poziomu, które należy wykonać w głównym wątku. Obejmuje to wszystkie wywołania metod kontekstu urządzenia Direct3D 11 (ID3D11DeviceContext).
  • StartLevel. Rozpoczyna rozgrywkę na nowy poziom.
  • PauseGame. Wstrzymuje grę.
  • RunGame. Uruchamia iterację pętli gry. Jest wywoływana z App::Update raz w każdej iteracji pętli gry, jeśli stan gry jest Aktywny.
  • OnSuspending i OnResuming. Wstrzymywanie/wznawianie dźwięku gry, odpowiednio.

Oto prywatne funkcje członków.

  • LoadSavedState i SaveState. Załaduj/zapisz odpowiednio bieżący stan gry.
  • LoadHighScore i SaveHighScore. Załaduj/zapisz odpowiednio wysoką ocenę w grach.
  • InitializeAmmo. Resetuje stan każdego obiektu sfery używanego jako amunicja z powrotem do pierwotnego stanu na początku każdej rundy.
  • UpdateDynamics. Jest to ważna metoda, ponieważ aktualizuje wszystkie obiekty gry na podstawie gotowych procedur animacji, fizyki i sygnałów sterujących. Jest to serce interakcyjności, która definiuje grę. Opisano to w sekcji Aktualizowanie świata gry.

Inne metody publiczne to akcesory właściwości, które zwracają informacje specyficzne dla rozgrywki i nakładek do platformy aplikacji w celu ich wyświetlenia.

Pola danych

Te obiekty są aktualizowane w miarę uruchamiania pętli gry.

  • obiekt MoveLookController. Reprezentuje dane wejściowe odtwarzacza. Aby uzyskać więcej informacji, zobacz Dodawanie kontrolek.
  • GameRenderer obiekt. Reprezentuje moduł renderujący Direct3D 11, który obsługuje wszystkie obiekty specyficzne dla urządzenia i ich renderowanie. Aby uzyskać więcej informacji, zobacz Rendering Framework I.
  • obiekt audio. Steruje odtwarzaniem dźwięku dla gry. Aby uzyskać więcej informacji, zobacz Dodawanie dźwięku.

Pl-PL: Pozostałe zmienne gry zawierają listy prymitywów i ich odpowiednie ilości w grze, a także dane specyficzne dla rozgrywki oraz ograniczenia rozgrywki.

Dalsze kroki

Musimy jeszcze omówić rzeczywisty silnik renderujący — jak wywołania metod Render na zaktualizowanych prymitywach są zamieniane w piksele na ekranie. Te aspekty zostały omówione w dwóch częściach —Framework renderowania I: Wprowadzenie do renderowania i Framework renderowania II: Renderowanie gier. Jeśli interesuje Cię to, jak sterowanie przez gracza aktualizuje stan gry, zobacz Dodawanie sterowania.