?

Log in

No account? Create an account
nyaload

Журнал Пушыстого

Журнал Пушыстого

Previous Entry Share Next Entry
C++, Именованные параметры функций в С++. Спрайты в 2d движках.
nyaload
_winnie
Блог же у меня вроде про программирование и разработку игр? И про C++?

Тут я изложу свой опыт того, как надо организовывать основную API-функцию 2d-игр: вывод прямоугольных спрайтов. Это основая функция, игра наполовину состоит из неё :) Остальная половина - игровая логика и чуточку прочего, вроде сейвов, звука, мат-библиотеки.
А, ещё билд ресурсов, шрифты, плавная анимация тоже важные темы, работа с которыми у программиста будет сильно влиять на скорость разработки.

Не знаю насколько, это будет полезно. Для PC лучше взять скриптуемый готовый движок, или флеш и положить игру в Facebook/вконтакт :)
Но если вдруг окажется, что начинаете возиться с таким низким уровнем, да ещё и на C++, тут можете увидеть советы, как сделать жизнь гейм-кодера слаще. Остальные могут увидеть удобный приём для языков с ООП (C++, Java, C#, JavaScript, Python, ...).

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

Спрайт можно нарисовать повёрнутый, умноженный на какой-нибудь цвет, полупрозрачный, растянутый по вертикали (или горизонтали), и ещё десяток других настроек. Часто я видел монстров вроде десятка перегруженных функций с десятком параметров, которыми всё равно неудобно пользоваться.
Draw(int x, int y, float rotation = 0, UINT32 color=-1, bool center=false, float scalex=1, float scaley=1, дофига).
"Рефакторинг" Фаулера учит нас, что если у функции двадцать параметров, то надо что-то менять в этой жизни. Наиболее прямолинейный способ - завести структуру DrawParams с 20-ю полями, конструктором по умолчанию, и сделать функцию Draw(const DrawParams ¶ms). Если сделать это без выдумки, опять будет неудобно. Тогда для простыни игрового кода, которая выводит спрайты, надо писать в три раза больше строчек, чем надо.

DrawParams  params;
params.rotation = PI/2;
params.color = 0xFFFFFF80; //полупрозрачность.
render.Draw(params)


А можно короче. Вот какой интерфейс у меня в результате получился:

render.Draw(DrawParams().AlphaB(0x80).Rot(PI/2)).


Как это сделать?
Я помимо полей в структуре DrawParams завёл ещё пачку методов. Которые возвращают *this как результат. И тогда эти параметры можно составлять вагончиками, и единым поездом пихнуть в функцию Draw.

В отличие от прямого использования полей, можно заводить разные проекции полей на методы. Например, есть функция Color(INT32) которая устанавлиет и RGB, и альфа канал, и отдельный метод RGB, и отдельный метод AlphaB для альфы от 0 до 255, и метод Alpha для float от 0 до 1.

В результате было вполне приятно печатать всякий код отрисовки бутербродов.

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


            drawTasks->Sprite(main_menu_cloud, x,y, TSpriteParam().Scale(2.f)); //один параметр, scale.

            //4 параметра
            drawTasks->Sprite(
                m_res->clash_flash, x, y, z,
                d3d::TSpriteParam()
                    .Center()
                    .Scale(_flash.GetVal()*3*_scale)
                    .Blend(d3d::S_SRCALPHA, d3d::D_ONE)
                    .Color(color::MakeRGBA(_r,_g,_b, 255*alpha))
                );


Озираясь назад, могу сказать, что x,y по отдельности в параметрах - неудобно, надо делать вектор. Из-за того что я сделал раздельно, мы по инерции писали что-то вроде Draw(sprite, some_position.x + some_offset.x * 10, some_position.y + some_offset.y * 10).
Возможно, звучит "очевидно", но в куче свободных движков это было именно два раздельных парметра - икс и игрек. z я бы всё равно оставил отдельно, он не ложится на векторные операции в 2d-играх.

А всякие вращения, скейлы, смещения спрайта надо хранить одной матрицей, а не отдельными float dx, float dy, scale x, scale y, float rotation - тогда не возникает вопросов "что будет, если я применю скейл, смещения, и вращение одновременно".

Вот что покрывает большинство потребностей в спрайтовой игре, :
Draw(
  pos, //в каком месте экрана рисовать
  z_order,  //параметр, по которому сортировать (см. ниже)
  DrawParams()
    .Rgb(255, 255, 255).AlphaB(255).Alpha(1.0)
    .Blend( ... ) //настройки блендинга для DirectX/OpenGL
    .BlendAdd() //90% - обычный бленд Sa+D(1-a) (должен быть по умолчанию). ещё 9% - адитивный блендинг.
    .AlignX(LEFT).AlignX(RIGHT).AlignX(CENTER)
    .AlignY(LEFT).AlignY(RIGHT).AlignY(CENTER)
    .Center() //центрование сразу и по X, и по Y
    .Rot(PI/2).Dx(10).Dy(-1).Offset(10, 20).Offset(vector2d)
    .ScaleX(2.0).ScaleY(2.0).Scale(2.0) //чаще всего нужен одинаковый Scale сразу по двум направлениям. Изредка разный по x-y осям.
    .MirrorX().MirrorY()
)


Ещё тонкость: первый параметр, pos - он должен быть вектор из двух float или из двух int?
Ответ: нужны обе функции Draw и DrawF. С явным различием в названии. Перегрузка Draw(float2d vec)+Draw(int2d vec) опасна и даёт неожиданные результаты: дерганное однопиксельное движение где хотим плавное движение облачка. И наоборот, замыленный спрайт из-за отсутствия округления float к целочисленным пикселям.

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


Ещё один важный момент. Сортировка спрайтов по Z. .

Есть следующие способы, по возрастанию сложности:
1) тупо отправляем спрайты в том порядке, в котором они поступают из кода.
2) указываем параметр z, по которому в дальнейшем отсортируем (z-buffer не годится из-за массовой полупрозрачности арта от художников).
3) начинаем составлять иерархические деревья, "вот этот спрайтик - потомок этого".

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

2) - у нас работало нормально. Иногда возникала проблема с тем, что начинали конфликтовать z-order объектов из совершенно разных модулей (типа гуёв и игровых объектов под ним), но без проблем решалась минимальной культурой хранения базовых смещений в общем .h-файле.

3) - сам не пробовал, не знаю насколько это оверкилл. Видел такое ActionScript и в PopCap, активно не пользовался. Подозреваю, может страдать проблемой 1, только не на этапе рисования, а на этапе создания этих деревьев из спрайтов. Когда что бы что-то втиснуть между спрайтами дерева из другого места нужно протаскивать указатель на это дерево через кучу мест. Можно ещё использовать 3+2 - потомки имеют z, по которому сортируются. Есть шанс просадить FPS.


см . так же сжатие графики для спрайтов
Tags: , ,

  • 1
(Deleted comment)
Fluent interface, надо запомнить. Сколько умных слов придумано, чтобы функциональный стиль в ООП запихнуть.

(Deleted comment)
Но на ООП я, наверное, зря наехал, это беда слабых языков.
В Smalltalk, небось, можно список сообщений сразу послать без многократного упоминания адресата.

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

в boost python'е так классы описываются

Хочу вот один старый проектик доделать. И, пользуясь случаем, несколько вопросов в тему спрайтового рендеринга:
- как шрифты лучше выводить?
- хочется поддерживать разные разрешения экрана. Для этого нужно как-то ресемплить спрайты при загрузке. Это дурацкая идея?
- посоветуешь вменяемую библиотеку для рисования спрайтов софтверно? Или может забить на софтверный рендеринг совсем?


Ох, очень большие вопросы, развёрнуто не могу ответить. И ссылок не могу сходу всопмнить, кроме как посылать в гугль.
Часто зависит от жанра игры, системных требований, ожиданий игрока, своих возможностей, используемого API/движка.

> как шрифты лучше выводить?
Ответ на вопрос полностью зависит от "используемого API/движка".
1) Рендерят на компьютере разработчика любым способом буквы в текстуру, сейвят в файл их ширину и текстурные координаты. Есть много готовых фонтогенераторов, уважающие себя движки предоставляют такую тулзу. Надписи рисуются прямоугольниками, с задаными текстурными координатами.
2) тоже самое, такую текстуру создают при загрузке при помощи библиотеки freetype
3) D3DXCreateFont
4) Готовая функция DrawString :)

> Хочется поддерживать разные разрешения экрана. Для этого нужно как-то ресемплить спрайты при загрузке. Это дурацкая идея?
Зависит от того, какая игра. Если нет жестких ограничений по памяти, можно забить, и просто использовать мип-мапы и средства видекарты.
Когда делают казуалки, обычно сцут делать под разные разрешения, выбирают одно фиксированное, сейчас можно смело выбирать 1024x768.
Не 3D игры и не-векторные игры очень сложно сделать под произвольные разрешения. Глупый ресемпл текстур - может получиться замыленное говно. А может и конфетка, если специально графику рисовать без деталей. Умные решения - сильно зависят от жанра игры.

> - посоветуешь вменяемую библиотеку для рисования спрайтов софтверно? Или может забить на софтверный рендеринг совсем?
На PC - забить, нарисовать треугольник с текстурой может любое офисно-ноутбучная видеокарта.
Можешь смело расчитывать на видеоускорение и 64мб видеопамяти.


В шарпе в синтаксис вынесли проблему - можно инициализировать поля в new Sprite{X=10, Y=20}

>>>> начинаем составлять иерархические деревья, "вот этот спрайтик - потомок этого".

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

В библиотеке boost такой способ установки параметров встречается. Последний раз я это видел в program_options.

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

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

При таком подходе можно делать чуднЫе вещи не столь большими усилиями, вот к примеру два наших стареньких продукта: http://screenseven.com/games/elven-mists-2/, http://screenseven.com/games/green-valley/. Если что - и там и там основная команда - 1 программист и 2 художника (причем разных).

Никто же в 3Д не занимается рукопашным пополигонным выводом, чем 2Д хуже?

это популярный метод. можно еще улучшить до чего-нибудь типа

sprite->at(pos).alpha(0x80).center().draw();

а именованные параметры в С++ это boost:params: http://www.boost.org/doc/libs/1_42_0/libs/parameter/doc/html/index.html

Аааа, живительный глоток цпп в моей ленте!

Видел ли это: http://www.run.montefiore.ulg.ac.be/~martin/resources/kung-f00.html ?

Кажется, решает все проблемы, красиво. Кажется, будет поддерживаться в VS2010.

Круто! Не видел, спасибо.

Методы всё-таки гибче и удобней :)
Можно указать пару параметров для метода,
Можно проецировать методы на поля как угодно:
1) внутри лежит матрица трансформации, а мы зовём Dx Dy Rot MirrorX
2) внутри лежит uint32_t color, а мы зовём Rgb(128, 128, 128).Rgb(color_t).AlphaF(1.0).AlphaB(255)

Насколько помню, именованые параметры нехило обсирались в C++ FQA:
http://yosefk.com/c++fqa/ref.html#fqa-8.4
http://yosefk.com/c++fqa/ctors.html#fqa-10.18

Если у вас у функции 20 разных параметров - то нужно менять архитектуру, а не гоняться за синтаксическим сахарком.

Смысл именованых параметров в том, что большая их часть остаётся по дефолту.

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

DrawParams CreateGUIElementParams(ALIGN alignment);
DrawParams CreateActorParams(vec2D coord);
DrawParams CreateBackgroundElementParams(vec2D coord, Scale scale);

И вот после этого уже, если есть сильное желание, можно для них писать именованые параметры - модификаторы.

что спрайты что 3д - суть сцена. отрисовка сцены хоробы делать в 1м месте. т.е. там можно и определять порядок. а как его лучше делать по Z или еще как- дело 10е вроде :)
про float-int - в CEGUI есть забавный макрос который float координаты выравнивает по пикселям, что бы не размывалось зря. т.е. можно все хратить в float и управлять выравниваением по пикселям. по мне так проще.

а касательно 1й части - когда спрайтов рисуется много - то наличие одной большой много-параметрой структуры может просадить перфоманс. :(
т.е. лучше иметь низко-уровневый механизм отрисовки квадов по 4м вершинкам, с цветом и бленд опом.
а над ним - удобные методы для разных видов _игровых_ спрайтов, что бы себе и другим жизнь облегчить :)
например если мне нужно рисовать 2 партиклы в больших кол-вах то добавлении лишних параметров в ф-ию которая будет вызывать раз так 20к в кадр - не очень гуд.

Аааа, смолток (и объективец) детектед!

http://www.boost.org/doc/libs/1_42_0/libs/parameter/doc/html/index.html
Велосипед детектед? :)

Re: boost.parameter

Нет.
У меня простой C++ без инклудов, там же взрыв мозга, который вменяемый лид не даст использовать на работе.

  • 1