Jak mogę w bezpieczny sposób obsługiwać komunikację między komponentami i przy użyciu pamięci komponentu przyjaznej dla pamięci podręcznej?

9

Tworzę grę, która wykorzystuje obiekty gier oparte na komponentach, i trudno mi wdrożyć sposób komunikacji każdego komponentu z jego obiektem gry. Zamiast wyjaśniać wszystko naraz, wyjaśnię każdą część odpowiedniego przykładowego kodu:

class GameObjectManager {
    public:
        //Updates all the game objects
        void update(Time dt);

        //Sends a message to all game objects
        void sendMessage(Message m);

    private:
        //Vector of all the game objects
        std::vector<GameObject> gameObjects;

        //vectors of the different types of components
        std::vector<InputComponent> input;
        std::vector<PhysicsComponent> ai;
        ...
        std::vector<RenderComponent> render;
}

GameObjectManagerPosiada wszystkie obiekty gry i ich komponentów. Odpowiada również za aktualizację obiektów gry. Odbywa się to poprzez aktualizację wektorów składowych w określonej kolejności. Używam wektorów zamiast tablic, więc praktycznie nie ma ograniczenia co do liczby obiektów gry, które mogą istnieć jednocześnie.

class GameObject {
    public:
        //Sends a message to the components in this game object
        void sendMessage(Message m);

    private:
        //id to keep track of components in the manager
        const int id;

        //Pointers to components in the game object manager
        std::vector<Component*> components;
}

GameObjectKlasa wie, jakie są jej składniki i może wysyłać do nich wiadomości.

class Component {
    public:
        //Receives messages and acts accordingly
        virtual void handleMessage(Message m) = 0;

        virtual void update(Time dt) = 0;

    protected:
        //Calls GameObject's sendMessage
        void sendMessageToObject(Message m);

        //Calls GameObjectManager's sendMessage
        void sendMessageToWorld(Message m);
}

ComponentKlasa jest czysto wirtualne, dzięki czemu zajęcia dla różnych typów elementów można wdrożyć jak obsługiwać wiadomości i aktualizacji. Jest również w stanie wysyłać wiadomości.

Teraz pojawia się problem, w jaki sposób komponenty mogą wywoływać sendMessagefunkcje w GameObjecti GameObjectManager. Wymyśliłem dwa możliwe rozwiązania:

  1. Daj Componentwskaźnik do tego GameObject.

Ponieważ jednak obiekty gry znajdują się w wektorze, wskaźniki mogą szybko zostać unieważnione (to samo można powiedzieć o wektorze w GameObject, ale mam nadzieję, że rozwiązanie tego problemu może również rozwiązać ten problem). Mógłbym umieścić obiekty gry w tablicy, ale wtedy musiałbym podać dowolną liczbę dla wielkości, która z łatwością mogłaby być niepotrzebnie wysoka i marnować pamięć.

  1. Daj Componentwskaźnik do GameObjectManager.

Nie chcę jednak, aby komponenty mogły wywoływać funkcję aktualizacji menedżera. Jestem jedyną osobą pracującą nad tym projektem, ale nie chcę mieć zwyczaju pisania potencjalnie niebezpiecznego kodu.

Jak mogę rozwiązać ten problem, jednocześnie zachowując kod i pamięć podręczną w bezpieczny sposób?

AlecM
źródło

Odpowiedzi:

6

Twój model komunikacji wydaje się dobry, a pierwsza opcja działałaby dobrze, gdybyś tylko mógł bezpiecznie przechowywać te wskaźniki. Możesz rozwiązać ten problem, wybierając inną strukturę danych do przechowywania komponentów.

std::vector<T>Była rozsądna pierwszym wyborem. Problemem jest jednak zachowanie unieważnienia iteratora kontenera. To, czego chcesz, to struktura danych, która jest szybka i spójna z pamięcią podręczną podczas iteracji, a także zachowuje stabilność iteratora podczas wstawiania lub usuwania elementów.

Możesz zbudować taką strukturę danych. Składa się z połączonej listy stron . Każda strona ma stałą pojemność i mieści wszystkie swoje elementy w jednym szeregu. Liczba jest używana do wskazania, ile elementów w tej tablicy jest aktywnych. Strona posiada również bezpłatną listę (umożliwiając ponowne wykorzystanie oczyszczonych Wpisy) oraz listę SKIP (pozwalające na pominięcie przez rozliczone pozycje podczas iteracji.

Innymi słowy, koncepcyjnie coś takiego:

struct Page {
   int count;
   int capacity;           // Optional if every page is a fixed size.
   T * m_storage;
   bool * m_skip;          // Skip list; can be bit-compressed.
   std::stack<int> m_free; // Can be replaced with a specialized stack.

   Page * next;
   Page * prior;           // Optional, allows reverse iteration
};

Niewyobrażalnie nazywam tę strukturę danych książką (ponieważ jest to zbiór stron, które iterujesz), ale struktura ma różne inne nazwy.

Matthew Bentley nazywa to „kolonią”. Implementacja Matthew wykorzystuje pole pominięcia liczenia skoków (przeprosiny za link MediaFire, ale właśnie w ten sposób sam Bentley hostuje dokument), co jest lepsze od bardziej typowej listy pominięć opartej na logice w tego rodzaju strukturach. Biblioteka Bentleya jest tylko w nagłówku i łatwo ją wpakować do dowolnego projektu w C ++, więc radzę po prostu użyć tego zamiast tworzyć własne. Połyskuję tutaj wiele subtelności i optymalizacji.

Ponieważ ta struktura danych nigdy nie przenosi elementów po ich dodaniu, wskaźniki i iteratory do tego elementu pozostają ważne, dopóki sam ten element nie zostanie usunięty (lub sam pojemnik nie zostanie wyczyszczony). Ponieważ przechowuje fragmenty sąsiadująco przydzielonych elementów, iteracja jest szybka i w większości spójna z pamięcią podręczną. Wstawianie i usuwanie jest uzasadnione.

To nie jest idealne; możliwe jest zniszczenie spójności pamięci podręcznej za pomocą wzorca użycia, który polega na silnym usuwaniu z efektywnie losowych miejsc w kontenerze, a następnie iteracji nad tym kontenerem, zanim kolejne wkładki mają wypełnione elementy. Jeśli często jesteś w tym scenariuszu, pomijasz potencjalnie duże regiony pamięci na raz. Jednak w praktyce uważam, że ten pojemnik jest rozsądnym wyborem dla twojego scenariusza.

Inne podejścia, które pozostawię do omówienia innym, mogą obejmować podejście oparte na uchwytach lub strukturę typu mapowania szczelin (gdzie macie tablicę asocjacyjną liczb całkowitych „kluczy” do liczb całkowitych „wartości”, przy czym wartości są indeksami w macierzy bazowej, która pozwala na iterację po wektorze poprzez ciągły dostęp przez „indeks” z pewną dodatkową pośrednią).


źródło
Cześć! Czy są jakieś zasoby, w których mogę dowiedzieć się więcej o alternatywach dla „kolonii”, o których wspomniałeś w ostatnim akapicie? Czy są wdrażane gdziekolwiek? Od jakiegoś czasu badam ten temat i jestem bardzo zainteresowany.
Rinat Veliakhmedov
5

Bycie „przyjaznym dla pamięci podręcznej” jest zajęciem dużych gier . Wydaje mi się to przedwczesną optymalizacją.


Jednym ze sposobów rozwiązania tego problemu bez „buforowania pamięci podręcznej” byłoby utworzenie obiektu na stercie zamiast na stosie: użyj newi (inteligentnych) wskaźników dla swoich obiektów. W ten sposób będziesz mógł odwoływać się do swoich obiektów, a ich odniesienie nie zostanie unieważnione.

Aby uzyskać rozwiązanie bardziej przyjazne dla pamięci podręcznej, możesz samodzielnie zarządzać cofaniem / alokacją obiektów i używać uchwytów do tych obiektów.

Zasadniczo, podczas inicjalizacji programu, obiekt rezerwuje kawałek pamięci na stercie (nazwijmy go MemMan), a następnie, gdy chcesz utworzyć komponent, mówisz MemMan, że potrzebujesz komponentu o rozmiarze X, to „ Zarezerwuję go dla ciebie, utwórz uchwyt i zachowaj wewnętrznie, gdzie w jego alokacji znajduje się obiekt tego uchwytu. Zwróci uchwyt i jedyną rzeczą, którą zatrzymasz na temat obiektu, nigdy wskaźnik do jego położenia w pamięci.

Gdy potrzebujesz komponentu, poprosisz MemMan o dostęp do tego obiektu, co chętnie zrobi. Ale nie zachowuj odniesienia do tego, ponieważ ...

Jednym z zadań MemMan jest utrzymywanie obiektów blisko siebie w pamięci. Raz na kilka klatek gry możesz nakazać MemManowi zmianę kolejności obiektów w pamięci (lub może to zrobić automatycznie podczas tworzenia / usuwania obiektów). Zaktualizuje swoją mapę lokalizacji uchwyt do pamięci. Twoje uchwyty zawsze będą ważne, ale jeśli zachowałeś odniesienie do przestrzeni pamięci ( wskaźnik lub odnośnik ), znajdziesz tylko rozpacz i pustkę.

Podręczniki mówią, że ten sposób zarządzania pamięcią ma co najmniej 2 zalety:

  1. mniej braków pamięci podręcznej, ponieważ obiekty są blisko siebie w pamięci i
  2. zmniejsza liczbę wywołań de / alokacji pamięci wykonywanych w systemie operacyjnym, o których mówi się, że zajmują trochę czasu.

Należy pamiętać, że sposób korzystania z MemMan i sposób wewnętrznej organizacji pamięci zależy od sposobu korzystania z komponentów. Jeśli będziesz iterował przez nie w oparciu o ich typ, będziesz chciał zachować komponenty według typu, jeśli iterujesz przez nie w oparciu o ich obiekt gry, musisz znaleźć sposób, aby upewnić się, że są blisko inny oparty na tym itp.

Vaillancourt
źródło