Как мы утверждали интерфейсы звукового модуля

Категория: Дневник разработки | Скилл: C++ , ООП , SkyXEngine | Дата: 07.05.2020
34 день разработки модуля работы со звуком, вчера я добрался до составления интерфейсов, а сегодня мы закончили их утверждать, это было крайне тяжелое время, работы по коду сделано очень, очень мало, зато больше 5 часов было проведено в спорах и уточнениях. Итоговый результат конечно же впечатляет, это минималистичный интерфейс, под которым реализовано много функционала, но так было не сразу ...

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

Так как у нас 3D движок, значит у нас есть и 2D (фоновые) звуки, а значит надо сделать четкое разделение этих понятий. А разделяет их параметр панорамирования (смещение между ушами слушателя):

  • 2D звукам можно задать вручную
  • 3D звуки автоматически панорамируются в зависимости от своей позиции в мире и позиции игрока, его направлению взгляда и верхнему вектору
В коде это выглядит так:
class ISoundBase2D: public IXUnknown
{
public:

	virtual XMETHODCALLTYPE void setPan(float fPan) = 0;
	virtual XMETHODCALLTYPE float getPan() const = 0;
};

//**************************************************************************

class ISoundBase3D: public IXUnknown
{
public:

	virtual XMETHODCALLTYPE const float3* getWorldPos() const = 0;
	virtual XMETHODCALLTYPE void setWorldPos(const float3 *pPos) = 0;

	virtual XMETHODCALLTYPE float getDistance() const = 0;
	virtual XMETHODCALLTYPE void setDistance(float fDist) = 0;
};

То есть, для ISoundBase3D бессмыслены методы ISoundBase2D и наоборот.

Ситуацию усугубляло наличие двух разных понятий обьектов проигрывания звука:

  • эмиттер - проигрыватель звука без возможности управления проигрыванием, но с возможностью задания минимальных параметров (громкость, панорамирование (2D, 3D), и прочее), его основная функция воспроизводить один и тот же звук несколько раз не дожидаясь его завершения (например стрельба из автомата)
  • плеер - полноценный проигрыватель с возможностью контроля проигрывания (пауза, возобновление, остановка, изменение/получение параметров)
Очень кратко кодом:
class ISoundBase: public IXUnknown
{
public:

	virtual XMETHODCALLTYPE void setVolume(float fVolume) = 0;
	virtual XMETHODCALLTYPE float getVolume() const = 0;
};

//**************************************************************************

class ISoundEmitterBase: public ISoundBase
{
public:

	virtual XMETHODCALLTYPE void play() = 0;
};

//**************************************************************************

class ISoundPlayerBase: public ISoundBase
{
public:

	virtual XMETHODCALLTYPE void setState(SOUND_STATE state) = 0;
	virtual XMETHODCALLTYPE SOUND_STATE getState() const = 0;

	virtual XMETHODCALLTYPE void setLoop(AB_LOOP loop) = 0;
	virtual XMETHODCALLTYPE AB_LOOP getLoop() const = 0;

	//! в млс
	virtual XMETHODCALLTYPE uint32_t getPlayPos() const = 0;
	virtual XMETHODCALLTYPE void setPlayPos(uint32_t uPosMls) = 0;
};

Как понятно из кода (еще раз уточним), предназначение эмиттера проиграть звук с заранее установленными настройками, в то время как у плеера задача управляемого проигрывания (состояния проигрывания, зацикленность, позиционирование и это я еще молчу о потоковом воспроизведении, которое невозможно у эмиттера).

А теперь скрестим эти две категории (2D, 3D)|(эмиттер, плеер) и получим ... да в принципе нормальный результат получим если применим множественное наследование (не ромбовидное, ну а внутри реализации конечно же ромб) типа так:

class ISoundEmitter2D: public ISoundEmitterBase, ISoundBase2D
{
};

class ISoundEmitter3D: public ISoundEmitterBase, ISoundBase3D
{
};

//**************************************************************************

class ISoundPlayer2D: public ISoundPlayerBase, ISoundBase2D
{
};

class ISoundPlayer3D: public ISoundPlayerBase, ISoundBase3D
{
};

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

Не долго думая (да хера, конкретно у меня это заняло более 2-х часов, сравнивая различные варианты и бурно обсуждая это в чате) было вынесено на рассмотрение несколько вариантов:

  • дублирование кода методов интерфейсов вместе с документацией, чтобы убрать множественное наследование, но оставить деление категорий
  • создание отдельных интерфейсов конфигов (2D, 3D) и обращение через специальные методы к ним
  • убрать деление на 2D и 3D, оставив базовый интерфейс звука, а эмиттер и плеер как разные не родные понятия
Первый вариант сходу не устраивал меня, (ну вдовль я уже накопипастился своего и чужого кода), хотя по ходу дела я почти согласился на одно маленькое копирование, но в конечном итоге нет.

Второй вариант, тот еще трэш (хоть и нивелирует факт копирования кода и документации, оставляя деление категорий):

class ISoundConfig2D: public IXUnknown
{
public:
	virtual XMETHODCALLTYPE void setPan(float fPan) = 0;
	virtual XMETHODCALLTYPE float getPan() const = 0;
};
class ISoundConfig3D: public IXUnknown
{
public:
	virtual XMETHODCALLTYPE const float3 getWorldPos() const = 0;
	virtual XMETHODCALLTYPE void setWorldPos(const float3 &vPos) = 0;

	virtual XMETHODCALLTYPE float getDistance() const = 0;
	virtual XMETHODCALLTYPE void setDistance(float fDist) = 0;
};

//**************************************************************************

class ISoundEmitter2D: public IXUnknown
{
public:
	virtual void XMETHODCALLTYPE play() = 0;
	
	virtual ISoundConfig2D* XMETHODCALLTYPE getConfig2D() = 0;
};

//**************************************************************************

//чтобы что-то изменить надо:
pEmitter->getConfig2D()->setDistance(10.f);

Но этот вариант еще и нарушал принцип построения интерфейса эмиттера, который в общем-то не должен предоставлять методы для получения какой-то внутренней информации, только установка и единственный метод play.

Нет, оба решили отказаться от D конфигов.

Убрать деление на 2D и 3D было единственным решением, которое могло разрешить текущую ситуацию и удовлетворить обе стороны, с некоторой долей терпимости и принятием решения не как лучший вариант, а как наименее худшее ... ну да, то есть лучшее в данной ситуации :)

Перед дальнейшим изложение сути, ознакомимся с кодом:

class ISoundBase: public IXUnknown
{
public:
	virtual SOUND_DTYPE XMETHODCALLTYPE getType() = 0;

	virtual void XMETHODCALLTYPE play() = 0;

	virtual void XMETHODCALLTYPE setVolume(float fVolume) = 0;
	virtual float XMETHODCALLTYPE getVolume() const = 0;

	virtual void XMETHODCALLTYPE setPan(float fPan) = 0;
	virtual float XMETHODCALLTYPE getPan() const = 0;

	virtual const float3& XMETHODCALLTYPE getWorldPos() const = 0;
	virtual void XMETHODCALLTYPE setWorldPos(const float3 &vPos) = 0;

	virtual float XMETHODCALLTYPE getDistance() const = 0;
	virtual void XMETHODCALLTYPE setDistance(float fDist) = 0;
};

//**************************************************************************

class ISoundEmitter: public virtual ISoundBase
{
};

//**************************************************************************

class ISoundPlayer: public virtual ISoundBase
{
public:
	virtual void XMETHODCALLTYPE resume() = 0;
	virtual void XMETHODCALLTYPE pause() = 0;
	virtual void XMETHODCALLTYPE stop() = 0;

	virtual bool XMETHODCALLTYPE isPlaying() const = 0;

	virtual AB_LOOP XMETHODCALLTYPE getLoop() const = 0;
	virtual void XMETHODCALLTYPE setLoop(AB_LOOP loop) = 0;

	virtual float XMETHODCALLTYPE getTime() const = 0;
	virtual void XMETHODCALLTYPE setTime(float fTime) = 0;

	virtual float XMETHODCALLTYPE getLength() const = 0;
};

Небольшое отступление. ISoundEmitter принимает все методы базового класса ISoundBase и не имеет своих, хотя можно было бы сделать эмиттер базовым классом и от него наследовать ISoundPlayer. Но следуя приниципу подстановки Барбары Лисков ("Наследующий класс должен дополнять, а не замещать поведение базового класса"), этого делать не стоит, потому что ISoundEmitter и ISoundPlayer это разные по логике и реализации обьекты, ISoundPlayer не будет дополнять, а лишь переопределит поведение ISoundEmitter.

А теперь сразу перейдем к главному минусу такого решения - отсутствие четкой дифференциации 2D и 3D, что влечет за собой наличие лишних методов и данных внутри реализации интерфейса для конкретного типа. Например для 2D звука нет необходимости хранить мировую позицию float3 (16 байт на каждый обьект, и на инстанс тоже), и методы ее получения/установки не имеют смысла. ИМХО, это может ввести в заблуждение неопытного пользователя, однако я любитель документации, поэтому умеющий читать будет предупрежден :) Но с другой стороны звуковой обьект будет создаваться в контексте того, что прямо говорит о типе звука. Например, у обьекта НПС не может быть фонового звука, а в главном меню не может быть пространсвенного.

Спойлер: мой неравнодушный партнер по разработке четко подметил, что можно сделать 2 разных реализации одного и того же интерфейса, однако, ИМХО, этого не стоит делать на данный момент, но возможно я просто устал и мне уже лень :)

Еще одним важным недостатком было наличие у эмиттера get методов, ну ладно, не будем на это смотреть, иначе мы так не придем ни к какому соглашению :\

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

33 и 34 дни разработки (почти все время выделенное на этот проект) были посвящены согласованию интерфейсов. Это были крайне тяжелые и изнурительные дни с томными часами бюрократических проволочек согласования того, что увидит пользователь движка. И я убежден, что он будет доволен, потому что это далеко не коленочная работа, а результат массивного мозгового штурма :)
Я Виталий, ник в сети Byurrer.
Увлекаюсь программированием, веду интересные проекты, пишу здесь об интересующих меня вещах: о работе, проектах, увлечениях и проффесиональном развитии.
Мое резюме

Проекты
SkyXEngine, PHP-API, S4G
Категории
В разработке :)
Популярное
В разработке :)