Пушыстый (_winnie) wrote,
Пушыстый
_winnie

Categories:

C++, Именованные параметры функций в С++. Спрайты в 2d движках.

Блог же у меня вроде про программирование и разработку игр? И про 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: c++, soft-dev, tips
Subscribe
  • Post a new comment

    Error

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 20 comments