Как правильно использовать синглтоны в программировании на С ++?


16

Я знаю, что одиночные игры плохие, мой старый игровой движок использовал одноэлементный объект «Game», который обрабатывает все, начиная от хранения всех данных и заканчивая реальным игровым циклом. Сейчас я делаю новый.

Проблема в том, чтобы нарисовать что-то в SFML, который вы используете window.draw(sprite) где находится окно sf::RenderWindow. Есть 2 варианта, которые я вижу здесь:

  1. Создайте одноэлементный игровой объект, который извлекает каждая сущность в игре (что я использовал раньше)
  2. Сделайте это конструктором для сущностей: Entity(x, y, window, view, ...etc)(это просто смешно и раздражает)

Каков будет правильный способ сделать это, сохранив конструктор Entity равным x и y?

Я мог бы попытаться отследить все, что я делаю в основном игровом цикле, и просто вручную нарисовать их спрайт в игровом цикле, но это тоже кажется грязным, и я также хочу получить абсолютный полный контроль над всей функцией отрисовки для сущности.


1
Вы можете передать окно в качестве аргумента функции render.
Дари

25
Синглтоны не плохие! они могут быть полезными , а иногда необходимо (конечно это спорно).
ExOfDe

3
Не стесняйтесь заменять синглтоны на простые глобалы. Нет смысла создавать глобально требуемые ресурсы «по требованию», нет смысла их передавать. Для сущностей вы можете использовать класс «level» для хранения определенных вещей, которые относятся ко всем из них.
snake5

Я объявляю свое окно и другие зависимости в моем main, а затем у меня есть указатели в других моих классах.
KaareZ

1
@JAB Легко исправляется при ручной инициализации из main (). Ленивая инициализация приводит к тому, что это происходит в неизвестный момент, что никогда не было хорошей идеей для базовых систем.
snake5

Ответы:


3

Сохраняйте только данные, необходимые для рендеринга спрайта внутри каждой сущности, затем извлекайте их из сущности и передавайте в окно для рендеринга. Нет необходимости хранить какие-либо окна или просматривать данные внутри сущностей.

У вас может быть класс Game или Engine верхнего уровня, который содержит класс Level (содержит все используемые в настоящее время сущности) и класс Renderer (содержит окно, представление и все остальное для рендеринга).

Таким образом, цикл обновления игры в вашем классе верхнего уровня может выглядеть так:

EntityList entities = mCurrentLevel.getEntities();
for(auto& i : entities){
  // Run game logic...
  i->update(...);
}
// Render all the entities
for(auto& i : entities){
  mRenderer->draw(i->getSprite());
}

3
Нет ничего идеального в синглтоне. Зачем делать внутренние реализации общедоступными, когда вам это не нужно? Зачем писать, Logger::getInstance().Log(...)а не просто Log(...)? Зачем инициализировать класс случайным образом, когда вас спрашивают, можете ли вы сделать это вручную только один раз? Глобальная функция, ссылающаяся на статические глобальные переменные, гораздо проще создавать и использовать.
snake5

@ snake5 Оправдание синглетонов на Stack Exchange похоже на сочувствие Гитлеру.
Вилли Козел

30

Простой подход состоит в том, чтобы просто сделать то, что раньше было Singleton<T>глобальнымT . У глобалов тоже есть проблемы, но они не представляют собой кучу дополнительной работы и шаблонного кода для обеспечения тривиального ограничения. Это в основном единственное решение, которое не предполагает (потенциально) касания конструктора сущностей.

Более сложный, но, возможно, лучший подход заключается в передаче ваших зависимостей туда, где они вам нужны . Да, это может включать передачуWindow * объектов (например, вашей сущности) способом, который выглядит грубым. Тот факт, что он выглядит грубым, должен вам кое-что сказать: ваш дизайн может быть грубым.

Причина, по которой это сложнее (помимо привлечения большего числа типов), заключается в том, что это часто приводит к рефакторингу ваших интерфейсов, так что то, что вам «нужно» передать, требуется меньшему количеству классов конечного уровня. Это делает многие уродства, присущие передаче вашего рендерера всему, исчезает, а также повышает общую удобство сопровождения вашего кода за счет уменьшения количества зависимостей и связности, степень которой вы сделали очень очевидной, принимая зависимости как параметры , Когда зависимости были единичными или глобальными, было менее очевидно, насколько взаимосвязаны ваши системы.

Но это потенциально серьезное начинание. Делать это в системе после факта может быть совершенно больно. Возможно, для вас гораздо более прагматично просто оставить свою систему наедине с одиночкой на данный момент (особенно если вы пытаетесь на самом деле выпустить игру, которая в противном случае работает просто отлично; игрокам, как правило, не будет интересно, если у вас есть). синглтон или четыре там).

Если вы действительно хотите попробовать сделать это с вашим существующим дизайном, вам может потребоваться опубликовать гораздо больше подробностей о вашей текущей реализации, поскольку на самом деле нет общего контрольного списка для внесения этих изменений. Или приходите обсудить это в чате .

Исходя из того, что вы опубликовали, я думаю, что большой шаг в направлении «без единого» будет состоять в том, чтобы избежать необходимости для ваших сущностей иметь доступ к окну или представлению. Это говорит о том, что они рисуют сами, а вам не нужно, чтобы сущности сами рисовали . Вы можете принять методологию, в которой сущности просто содержат информацию, которая позволила бы их нужно рисовать какой-то внешней системой (которая имеет ссылки на окна и представления). Сущность просто выставляет свою позицию и спрайт, который она должна использовать (или какая-то ссылка на указанный спрайт, если вы хотите кэшировать фактические спрайты в самом рендерере, чтобы избежать дублирования экземпляров). Рендереру просто приказывают нарисовать определенный список сущностей, который он просматривает, считывает данные и использует свой внутренний оконный объект для вызова drawсо спрайтом, ищущим сущность.


3
Я не знаком с C ++, но разве нет удобных структур внедрения зависимостей для этого языка?
bgusach

1
Я бы не назвал ни одного из них «удобным», и в целом я не нахожу их особенно полезными, но у других может быть разный опыт работы с ними, так что это хороший момент, чтобы поднять их.

1
Метод, который он описывает как создание так, чтобы сущности не рисовали их самостоятельно, а хранили информацию, и единственная система, обрабатывающая рисование всех сущностей, сегодня широко используется в большинстве популярных игровых движков.
Патрик В. МакМахон

1
+1 за «Тот факт, что он выглядит грубым, должен кое-что вам сказать: ваш дизайн может быть грубым».
Shadow503

+1 за идеальный случай и прагматичный ответ.

6

Наследовать от sf :: RenderWindow

SFML фактически поощряет вас наследовать от своих классов.

class GameWindow: public sf::RenderWindow{};

Отсюда вы создаете функции рисования элементов для объектов рисования.

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity);
};

Теперь вы можете сделать это:

GameWindow window;
Entity entity;

window.draw(entity);

Вы даже можете сделать это еще дальше, если ваши сущности собираются хранить свои уникальные спрайты, заставляя сущность наследовать от sf :: Sprite.

class Entity: public sf::Sprite{};

Теперь sf::RenderWindowможно просто рисовать сущности, а сущности теперь имеют такие функции, как setTexture()иsetColor() . Объект может даже использовать позицию спрайта в качестве своей собственной позиции, что позволяет вам использовать setPosition()функцию как для перемещения Объекта, так и для его спрайта.


В конце концов , это очень приятно, если у вас есть:

window.draw(game);

Ниже приведены несколько примеров реализации.

class GameWindow: public sf::RenderWindow{
 sf::Sprite entitySprite; //assuming your Entities don't need unique sprites.
public:
 void draw(const Entity& entity){
  entitySprite.setPosition(entity.getPosition());
  sf::RenderWindow::draw(entitySprite);
 }
};

ИЛИ

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity){
  sf::RenderWindow::draw(entity.getSprite()); //assuming Entities hold their own sprite.
 }
};

3

Вы избегаете одиночных игр в разработке игр так же, как вы избегаете их в любой другой разработке программного обеспечения: вы передаете зависимости .

При этом вы можете передавать зависимости напрямую как голые типы (например int, Window*и т. Д.) Или передавать их в один или несколько пользовательских типов-оболочек (например,EntityInitializationOptions ).

Первый способ может раздражать (как вы выяснили), а второй позволит вам передавать все в одном объекте и изменять поля (и даже специализировать тип параметров), не обходя и не изменяя каждый конструктор сущностей. Я думаю, что последний способ лучше.


3

Синглтоны не плохие. Вместо этого ими легко злоупотреблять. С другой стороны, глобальные ошибки даже легче использовать, и у них больше проблем.

Единственная действительная причина заменить синглтон глобальным - это умиротворение религиозных ненавистников-синглтонов.

Проблема состоит в том, чтобы иметь дизайн, который включает классы, из которых когда-либо существует только один глобальный экземпляр, и который должен быть доступен везде. Это распадается, как только у вас появляется несколько экземпляров синглтона, например, в игре, когда вы реализуете разделенный экран, или в достаточно крупном корпоративном приложении, когда вы замечаете, что один регистратор не всегда такая хорошая идея. ,

В итоге, если у вас действительно есть класс, где у вас есть один глобальный экземпляр, который вы не можете разумно передать по ссылке , синглтон часто является одним из лучших решений в пуле неоптимальных решений.


1
Я религиозный ненавистник-одиночка, и я не считаю глобальным решением. : S
Дэн Кладовая

1

Внедрить зависимости. Преимущество этого заключается в том, что теперь вы можете создавать различные типы этих зависимостей через фабрику. К сожалению, вырывать синглетов из класса, который их использует, все равно что тянуть кошку за задние лапы по ковру. Но если вы внедряете их, вы можете менять реализации, возможно, на лету.

RenderSystem(IWindow* window);

Теперь вы можете вводить различные типы окон. Это позволяет вам писать тесты для RenderSystem с различными типами окон, чтобы вы могли увидеть, как ваша RenderSystem сломается или будет работать. Это невозможно или более сложно, если вы используете синглтоны непосредственно внутри "RenderSystem".

Теперь он более тестируемый, модульный и также не связан с конкретной реализацией. Это зависит только от интерфейса, а не от конкретной реализации.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.