27 октября 2011

Drag'n'drop, промежуточный результат


Всем доброго дня!
Давно не писал в журнал, так как навалилась целая лавина дел, которые никак нельзя отложить, заморозить или просто делать в расслабленном режиме. Психологически нелегкое выдалось время, но я стараюсь разобраться со всем изо всех сил. А сегодня я решил побаловать себя, нашел капелюшочку сил и выделил несколько часов на описание демки Drag’n’drop (в рамках создания gui-демонстраций). Точнее сказать, это еще не окончательная программа, а всего лишь заготовка; но тем проще читателям будет понять те или иные моменты работы законченной демки, а мне намного удобнее разбивать описание работы сложных классов на несколько сообщений.
Итак, встречайте!
Сегодня делаем первую часть - нормальное перетаскивание объектов по экрану. То есть схватили мышкой и перетянули в другое место.

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

  TDragTarget = class;

  TBaseDrag = class
    fSprite: TGlHudSprite;
  end;

  TDragObject = class(TBaseDrag)
    fInTarget: TDragTarget;
  end;

  TDragTarget = class(TBaseDrag)
    fObjectInPlace: TDragObject;
  end;

Первая строка (TDragTarget = class;) является неким обходным маневром для рекуррентного использования классов в получившейся связке TDragObject <--> TDragTarget. То есть полем у одного класса является объект другого класса, и наоборот. А наш обходной маневр позволяет объявить класс TDragTarget в самом верху и уже спокойно использовать как корректное поле для класса TDragObject (я нормально объяснил?)

Так как объектов на экране будет много, то для удобства создадим класс-список, который будет хранить набор объектов типа TBaseDrag. Я назвал этот утилитарный класс TDragList, сейчас он выглядит совсем просто и умеет совсем немного:

  TDragList = class(TList)
    Function GetObjectByIndex(aIndex: Integer): TBaseDrag;
    Function GetObjectRect(aIndex: Integer): TVector;
    Function GetObjectByPos(aPos: TVector): TBaseDrag;
  end; 

Еще раз оговорюсь, сегодня делаем только передвижение объектов, без привязки к конечному месту, куда они кладутся; таким образом класс TDragTarget сегодня не пригодится, да и многое из того, что описано в текущей демке, возможно будет перерабатываться - но тем интереснее будет наблюдать за эволюцией кода, структуры и логики работы тех или иных моментов программы.

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

  TDragManager = class (TGlDummyCube)
  protected
    fTargets: TDragList;
    fObjects: TDragList;

    fLButtonDown: Boolean;
    fDragingObject: TDragObject;
    fDragingPos: TVector;
    fMousePos: TVector;

    Function GetObjectByIndex(aIndex: Integer): TDragObject;
  public
    Procedure DoRender(var ARci: TRenderContextInfo; ARenderSelf, ARenderChildren: Boolean); override;
    Procedure DoProgress(const progressTime: TProgressTimes); override;
    Procedure SetMousePos(aMousePos: TVector);

    Function AddDragObject(aPos: TVector; aMaterialName: String): TDragObject;
    Constructor Create(AOwner: TComponent); override;
  end;

Взгляд вперед
Как видно, это уже заготовка под большой класс, с помощью которого можно будет размещать драг-объекты (TDragObject'ы) по набору драг-целей (TDragTarget'ы). В результате, я надеюсь, что с помощью этого класса можно будет довольно быстро и легко накидать итоговую демку, в которой будем снаряжать персонажа различными объектами. Для видения конечного результата, я собрал картинку в FireWorks'е, обращаю внимание - это не скриншот:


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

Работа TDragManager'а
С помощью метода AddDragObject() можно с легкостью добавить в список драг-объектов еще один в заданных координатах и с определенной текстурой. Перемещением всех объектов заведует перекрытый метод DoProgress(), внутри которого, в зависимости от состояния мыши, производятся манипуляции с объектами. На текущий момент это выглядит так:

Procedure TDragManager.DoProgress(const progressTime: TProgressTimes);
var
  Obj: TDragObject;
begin
  if IsKeyDown(VK_LBUTTON) and not fLButtonDown then
  begin
    Obj := TDragObject(fObjects.GetObjectByPos(fMousePos));
    if Obj <> nil then
    begin
      fDragingObject := Obj;
      fDragingPos := VectorSubtract(Obj.fSprite.Position.AsVector, fMousePos);
    end;
  end;

  if fDragingObject <> nil then
    fDragingObject.fSprite.Position.SetPoint(VectorAdd(fDragingPos, fMousePos));

  if not IsKeyDown(VK_LBUTTON) and (fDragingObject <> nil) then
    fDragingObject := nil;

  fLButtonDown := IsKeyDown(VK_LBUTTON);
end;

Немного поясню, сказал, что в fDragingPos записывается смещение мыши от центра объекты в момент клика. Это необходимо, чтобы при движениях мыши сохранялось относительное положение объекта и курсора.

Предварительный итог
Теперь, в основной программе нам достаточно выполнить всего несколько действий (комментарии помещаю внутрь):

//создаем менеджер и устанавливаем библитеку материалов
  fDragManager := TDragManager.CreateAsChild(fMainDummy);
  fDragManager.Material.MaterialLibrary := fMatLib;

//добавляем нужное нам количество объектов в заданных координатах и с нужным материалом
  fDragManager.AddDragObject(VectorMake(100, 100, 0), 'dnd_item');
  fDragManager.AddDragObject(VectorMake(200, 150, 0), 'dnd_item');

А в основном обработчике OnCadencerProgress() передаем координаты курсора мыши:

fDragManager.SetMousePos(VectorMake(MPos.x, MPos.y, 0));

И... все! В итоге наш менеджер сам позаботится о перетаскивании созданных объектов. Сейчас демка выглядит так:


Схватит мышкой, можем перемещать квадратики по экрану... Просто? Дальше - больше, обещаю! :)
Итак, текущие изменения ознаменованы 14ой ревизией в репозитории демок. Отдельно файл uDragManager можно подсмотреть здесь.

p.s. Кстати, забыл сказать - во многих демках утекает память, так как не везде я позаботился о правильном освобождении выделенной памяти (все-таки демонстрации же). В ближайших сообщениях о постараюсь осветить момент, как я борюсь с утечками и обязательно почищу все демки на этот счет!

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

4 коммент.:

  1. Классный компонент. Теперь можно с легкостью делать функциональные инвентари в играх.

    ОтветитьУдалить
  2. На каком этапе ты получаешь fMousePos ?

    ОтветитьУдалить
  3. "я нормально объяснил?" ага =)
    потрясно! уже хочется продолжения :)

    ОтветитьУдалить
  4. gltrinix, спасибо! постараюсь сделать его максимально удобным...

    LOLO, у менеджера есть метод SetMousePos(), который вызывается "снаружи", прямо из главного каденсера демки...

    Radical Ed,
    здорово! продолжение постараюсь выложить в ближайшее время...

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