19 декабря 2011

GUI: Drag and drop

Я наконец сделал это!
Сегодня продолжаем делать маленькие хитрости для интерфейса, список которых я старался составить в зависимости от нужд и интересов читателей. И на очереди у нас перетаскивание объектов.
На нерусском это называется "Drag and drop".
Всем знаком процесс копирования файлов - перетягиваем файл из одной папки в другую. Наверно буду банален, сказав, что это и есть самый простой пример drag'n'drop'а.
Где такое встречается в играх?! Ситуаций много:
  • Копаясь в инвентаре мы перемещаем объекты с одного места на другое, более удобно располагая ту или иную вещь
  • При торговле зачастую используют систему drag'n'drop - игрок самостоятельно перетягивает объекты у торговца к себе в мешок, при этом плата снимается автоматически
  • В играх вроде Colonization экипировка корабля в порту осуществляется закидыванием грузов простым перетаскиванием иконок вроде Серебро или Табак в трюм корабля - удобно!
  • Во флешке Gem Craft можно совмещать драгоценные камни, наделяя их новыми способностями; таким образом можно получить "замедляюще-ядовитый камень"; объединение камней происходит как раз drag'n'drop'ом одного на другой
Как видим, примеров огромное количество, поэтому иметь такую штуковину в своем арсенале обязательно нужно...
хм... Приступим!


В самом начале рекомендую прочитать первую часть рассказа о разработке этой демки. А сегодня мы, наконец, доделаем нашу программу, создав рабочий вид класса TDragManager. В принципе, дело за малым - наполнить модуль uDragManager различными вкусностями, вроде "приемников" для перемещения объектов, срабатывания сообщений на основные события и остальные мелочи, так необходимые для удобного использования.

Чтобы не перенасыщать сообщение излишним кодом, который в итоге "спрятан" внутри нашего TDragManager'а, я буду стараться при возможности рассказывать, как использовать этот класс "снаружи", умалчивая про то, как же это устроено внутри. Надеюсь, бывалых программистов это не отпугнет :)

Сейчас вы меня спросите: "Не тяни, что же у нас нового, кэп?".
Итак, у нас появился вполне самостоятельный метод AddDragTarget(), который требует позицию цели, в которую можно будет класть драг-объект:

Function TDragManager.AddDragTarget(aPos: TVector; aMaterialName: String): TDragTarget;

После вызова этого метода, на экране появится драг-цель, в которую без труда можно будет поместить любой драг-объект, добавленный в менеджер с помощью AddDragObject()
(в первой части рассказа только метод AddDragObject и был, теперь же появился и AddDragTarget; а вместе они дополняют друг друга).
Стоит отметить, что прежде активной областью драг-объекта была вся текстура, таким образом, если на текстуре 128х128 пикселей в центре был маленький объект размерами 70х70, то его можно было "таскать" по экрану за его прозрачные границы. Сейчас же, чтобы такого недоразумения не происходило, я добавил метод SetActionRect(), который принимает четыре значения для указания прямоугольника, реагирующего на клики пользователя. К примеру, передача четырех значения -1, -1, 1, 1 укажет объекту, что необходимо использовать целиковую текстуру, то есть драг-объект будет реагировать как и раньше.
С учетом этого изменения, добавление драг-объектов и драг-целей на экран в нашей демке выглядит довольно красиво:

  // объявляем вспомогательную функцию
  Function AddCloneDragObject(aLeft, aTop: Single; aMatName: String; aPercents: Single): TDragObject;
  begin
    result := fDragManager.AddDragObject(VectorMake(aLeft, aTop, 0), aMatName);
    with result do
    begin
      Cloning := true;
      SetActionRect(-aPercents, -aPercents, aPercents, aPercents);
    end;
  end;
  ...
  // добавляем драг-цели
  fDragManager.AddDragTarget(VectorMake(300, 120, 0), 'dnd_item');
  ...
  // добавляем драг-объекты
  AddCloneDragObject(50,  40,  'cup', 0.5);
  ...

Правда просто?
Наверно сразу возник вопрос,  что делает строка

Cloning := true

Дело в том, что зачастую требуется перетаскивать объект, создавая его копию при старте перетаскивания. Достаточно вспомнить торговцев из RPG-игр с нескончаемым запасом товара. Покупаешь у него блестящий меч, но у него в лавке заветный предмет никуда не пропадает, можно купить еще и еще!
Так вот у нас также! Выставляя свойство Cloning в true, мы указываем объекту на необходимость создания нового экземпляра при старте драга. Скриншот результата:


Прямо-таки нашествие варежек!
Справа виднеется корзина, в которую можно положить ненужный объект, который тотчас же пропадет в неизвестном направлении. Добиться подобного эффекта можно отслеживанием различных событий. Нам достаточно слушать конец драга:

  fDragManager.OnEndDrag := OnEndDrag;

А в обработчике как раз проверяем, куда мы кладем драг-объект; если это корзина, убиваем объект из списка:

Procedure TfrmMain.OnEndDrag(aManager: TDragManager; aDragObject: TDragObject; aDragTarget: TDragTarget; aPreviousTarget: TDragTarget);
begin
  // если кладем объект в корзину...
  if aDragTarget = fBasket then
  begin
    // ...то навеки убиваем его!
    aManager.DeleteByObject(aDragObject, true);
    aDragObject := nil;
  end;

  // если мы пытаемся положить объект "высоко", то также убиваем его!
  if (aDragObject <> nil) and (aDragObject.fSprite.Position.y < 80) then
    aManager.DeleteByObject(aDragObject, true);
end;

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

    // on start drag
    property OnBeginDrag: TOnDragBaseEvent
    // on end drag
    property OnEndDrag: TOnDragProceedEvent
    // on moving drag-object
    property OnDragProceed: TOnDragBaseEvent
    // on change target while dragging
    property OnDragProceedTarget: TOnDragProceedEvent

Пока мы используем только OnEndDrag, но остальные тоже могут пригодиться в любой момент! Если появится необходимость - сообщайте в комментариях, с радостью постараюсь добавить!
Экипируем своего персонажа:
На этом, пожалуй, все... Качаем демку (560kb) или обновляемся из репозитория (18ая ревизия).
Видео происходящего:

Сообщения, схожие по тематике:

5 коммент.:

  1. Выше всяких похвал. Браво. И как всегда очень мило)))

    ОтветитьУдалить
  2. gltrinix,
    спасибо за добрые слова!
    правда всего лишь две демки за три месяца с момента составления списка - это как-то долговато... а впереди ведь много всего интересного!

    ОтветитьУдалить
  3. Отлично!
    В твоих статьях и примерах привлекает стильный и простой дизайн :)

    ОтветитьУдалить
  4. Замечательно!
    Если в занятое поле положить объект из верхней части, то в верхней панельке объекты "накладываются" друг на друга. Что-то мне говроит, что так изначально не было задумано...(маленький баг-фикс)
    Спасибо за демку) может в скором времени понадобиться.

    ОтветитьУдалить
  5. perfect daemon,
    благодарю! я считаю, что арт для игры - это не рисование, это дизайн... стараюсь не падать в грязь лицом!

    Ulop,
    очень рад, если эти демки будут полезными!
    баг принят на заметку - как будет время, постараюсь поправить! большое спасибо!

    ОтветитьУдалить