Разрабатывая собственную игру, рано или поздно приходится задумываться о таком способе размещения объектов на сцене, при котором инфорацию о сцене можно будет хранить во внешнем файле.
На первоначальном этапе разработке вполне нормально увидеть в игре такой код:
1 2 3 4 5 6 |
... world.Insert(new Wall(Vector2D(250, 300), Vector2D(80, 20))); world.Insert(new Wall(Vector2D(250, 200), Vector2D(80, 20))); world.Insert(new Wall(Vector2D(200, 250), Vector2D(20, 80))); world.Insert(new LightEmitter(Vector2D(230, 450))); world.Insert(new PlayerSpawnPoint(Vector2D(230, 450))); |
Однако, рано или поздно игра вырастает больше чем в техническую демку и нам будет нужно редактировать уровни чаще чем код. Так или иначе, нам придётся хранить уровни в каком-то внешнем хранилище, будь то база данных, xml-файлы или же бинарные файлы своего формата.
Дело ясно: нам нужен менеджер уровней, который будет считывать данные об объектах из внешнего хранилища и создавать их на уровне.
Звучит просто, но на деле есть одна неприятная проблема: для того чтобы была возможность создать объект по идентификатору, менеджер уровней должен знать конструкторы всех создаваемых классов и иметь возможность выбирать из них нужные.
switch-case
Для простоты предположим, что файл уровня имеет следующую структуру
1 2 3 4 |
Wall 190 180 Wall 190 250 Lamp 150 30 ... |
Самый простой способ обработать такие данные, это использовать конструкцию switch-case (if .. else if для строковых идентификаторов в нашем случае)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
... while (!file.eof()) { file >> classId; file >> pos.x; file >> pos.y; if (classId == "Wall") { newActor = new Wall(pos); } else if (classId == "Lamp") { newActor = new Lamp(pos); } ... |
Первое время такой код может выглядеть достаточно неплохо, но с рано или поздно он непременно разрастётся в огромное полотно кода, в котором будет сложно что-то найти.
Паттерн фабрика объектов
Данный паттерн достаточно подробно описан в книге Александреску А. “Современное проектирование на C++”. Поэтому, я не буду особенно касаться реализации данного паттерна.
В конечном счёте, я реализовал свою Фабрику аналогично той, что описана в книге. Но изначально я не понял идеи синглтона и реализовал методы фабрики в виде “статического класса”, а затем поплатился за это.
Суть фабрики в следующем:
- Фабрика хранит конструкторы в талбице вида “ключ-значение”, где ключом является идентификатор нужного типа.
- Перед использованием фабрики необходимо зарегистрировать в ней все конструкторы.
Конструкторы могут быть зарегистрированы в любом месте программы, однако, наиболее удобным я считаю регистрацию в *.cpp файле того класса, чей конструктор регистрируется. Это позволяет исключать регистрацию в фабрике вместе с удалением класса, а так же, держать весь зависимый код в одном месте.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#ifndef ACTOR_FACTORY #define ACTOR_FACTORY #include <map> #include "../Core/World.h" /** * Factory that allows to create any type of registered actor by their name identifier. * * Based on pattern Factory in book Andrei Alexandrescu "Modern C++ Design: Generic Programming and Design Patterns Applied" * * ActorFactory is a singleton. */ class ActorFactory { public: typedef IActor* (*CreateActorCallback)(World *world, const Vector2D location, const Vector2D scale, const Rotator rotation); private: typedef std::map<std::string, CreateActorCallback> CallbackMap; public: /** * @return the single instance of the ActorFactory */ static ActorFactory& Factory(); /** * register new actor class by an identifier and a "create" function * @return true if class was not registered earlier */ bool registerActor(std::string actorId, CreateActorCallback createFn); /** * unregister already registred class * @return true if class was unregistered */ bool unregisterActor(std::string actorId); /** * Create actor by identifier. * * @throws runtime_error when actorId is unknown */ IActor* createActor(std::string actorId, World *world, const Vector2D location, const Vector2D size, const Rotator rotation); private: CallbackMap callbacks; /* * Turn off unusable operations */ ActorFactory(); ActorFactory(const ActorFactory&); ~ActorFactory(); ActorFactory& operator=(const ActorFactory&); }; #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include "ActorFactory.h" ActorFactory::ActorFactory() {} ActorFactory::~ActorFactory() {} ActorFactory& ActorFactory::Factory() { static ActorFactory singleInstance; return singleInstance; } bool ActorFactory::registerActor(std::string actorId, CreateActorCallback createFn) { return this->callbacks.insert(CallbackMap::value_type(actorId, createFn)).second; } bool ActorFactory::unregisterActor(std::string actorId) { return this->callbacks.erase(actorId) == 1; } IActor* ActorFactory::createActor(std::string actorId, World *world, const Vector2D location, const Vector2D scale, const Rotator rotation) { CallbackMap::const_iterator it = this->callbacks.find(actorId); if (it == this->callbacks.end()) { throw std::runtime_error("Unknown actor identefier"); } return (it->second)(world, location, scale, rotation); } |
В каждый файл *.cpp классов, которые должны быть зарегистрированы в фабрике, добавляется следующий код.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// unnamed namespase to hide from another places namespace { // specific factory IActor* CreateBullet(World *world, const Vector2D location, const Vector2D scale, const Rotator rotation) { return new Bullet(world, location, scale, rotation); } const std::string CLASS_ID = "Bullet"; // register specific factory in actor factory const bool registered = ActorFactory::Factory().registerActor(CLASS_ID, CreateBullet); } |
UPD: Проблема со статическими библиотеками
Проявилась следующая проблема:
При вынесении в статические библиотеки кода классов, которые должны регистрироваться в фабрике, линковщик может не подключать объектные файлы таких классов, если посчитает их неиспользуемыми.
Решения подсказали два:
- Выносить регистрации в отдельный файл в проекте конечного приложения.
- Использовать параметр компоновщика, запрещающий исключать код библиотек при линковке.
Решения не очень красивые, но в первом варианте в отличие от случая со switch-case, регистрации можно производить в конечной программе, что не так затратно как модификация блоков swith-case в библиотеке.
Leave a Reply
You must be logged in to post a comment.