Skip to content

Drag & Drop module

ivan-mogilko edited this page Jan 31, 2024 · 15 revisions

DragDrop and DragDropCommon are two modules that make it possible to drag things around in AGS. What is the difference between these two?

DragDrop module implements an abstract drag-and-drop functionality. This means that the module does not drag any actual game object on its own, but rather calculates the dragging and dropping action, and tells you what and how happens, so that you could script the actual object movement. You might say, this module provides an idea of dragging, rather than dragging itself :).

The upside of such approach is that you may script dragging of anything, both standard AGS objects and your own custom things, even non-ordinary pseudo-objects. For example, you may use this module to script dragging a line from point A to B, or draw a rectangle by dragging one of it's corners. The downside is that you have to be ready to do some extra work, and understand scripting well.

When you need to just make normal ordinary game objects dragged around in your game, this is what DragDropCommon module is for. DragDropCommon supports drag-and-drop for Characters, Room Objects, GUIs and GUI controls, and Inventory Items, right out of the box, only with a minimal setup.

NOTE: DragDropCommon module actually depends on DragDrop module, so you will have to add both to your game if you want to use DragDropCommon.

See also:

Quick example

Just to demonstrate the use of these modules, here's the quick example:

  1. Add both DragDrop and DragDropCommon modules to your game. DragDrop should be higher in the module list, or your game won't compile.
  2. Go to the room editor, and add "After Fade-in" event handler. In script put following code:
    function room_AfterFadeIn() {
        // configure base DragDrop module
        DragDrop.Enabled = true;
        DragDrop.DefaultHookMouseButton = eMouseLeft; // use left mouse button to drag
        DragDrop.DragMinDistance = 3; // drag after mouse moved at least 3 pixels away
        // now configure DragDropCommon to let drag real objects
        DragDropCommon.ModeEnabled[eDragDropCharacter] = true; // drag characters
        DragDropCommon.ModeEnabled[eDragDropRoomObject] = true; // drag room objects
        DragDropCommon.DragMove = eDDCmnMoveGhostOverlay; // drag using sprite's copy on overlay
    }
  1. Just in case, add following code to the "Leaves room" event:
    function room_Leave() {
        DragDropCommon.DisableAllModes();
        DragDrop.Enabled = false;
    }
  1. That's it! Now you can drag and drop characters and objects with your mouse in that room.

Basic terms

Term Meaning
Draggable object This is something that can be dragged to another position.
Hooking Is an event of "grappling up" the draggable object to move it somewhere.
Hook Key Is the key or mouse button that should be pressed down to hook an object.
Cancel The event of "unhooking" an object before the dragging even started.
Dragging Is the process of moving hooked object around while the hook key is down; NOTE: it does not have to literally move an actual object on screen, it may be a graphical copy, or a "pseudo-object", and so on.
Drop The event of putting an object onto new position; usually occurs when the hook key was released, but may also be triggered by script command.
Revert The event of returning an object to the old position; for example may be scripted to happen if the drop position is invalid.

The drag-and-drop action consists of several steps or phases. In a nutshell it looks like this:

Step Meaning
Idle First, we wait for a player to press a Hook Key down.
Hooking As soon as the key is pressed, we look for a suitable object under the mouse cursor. If failed, we return to Idle.
Waiting for drag After the object got "hooked", we're waiting for drag conditions to match, such as minimal mouse move distance or a time the hook key have been held down.
Cancel If the hook key was released before the drag conditions are met, the drag is cancelled and the hooked object remains in place. Then we return to Idle.
Dragging The object is being dragged around. It takes releasing hook key or explicit command for a dragging to stop.
Dropping If the hook key was released while dragging the object, we're checking if the new position is suitable for placing the object or not. If not, then the object will be returned to the old position (reverted).
After dropped This is one extra game tick before returning to Idle mode, at this point we know the result of the drop (drop or revert).

Using DragDrop

Like was said earlier, DragDrop module itself provides an abstract functionality, where it only tests for the player's input, keeps the record of current situation, and tells you what should happen each moment in time, while it is up to you to handle such events correspondingly: provide description of a draggable object under the cursor, update the real object's position as it is being dragged around, and so on.

See also:

DragDrop works in two modes: automatic and manual, where the automatic mode means that it listens to player's input itself, and in the manual mode you may send input commands to DragDrop as you see fit. These modes may also be mixed, where one part is done automatically and another is controlled by you. This is explained in more detail below.

But before we get to that, we need to quickly discuss how the module's events are handled.

Signal properties

Since AGS does not support custom callbacks (that is: calling your own functions when something happened in script module), DragDrop tells you about immediate events by setting certain "signal" properties. These properties are easily distinguished by Evt prefix. For example, a property may be called EvtDragStarted, which tells that an object is begun being dragged. A signal property may have a value of either 1 or 0 (or true and false). The positive value is only set for 1 game tick (update), and is reset immediately afterwards. They mean that the event has just occured, and you should react to that. The next tick they will be reset until some event occurs again.

You will have to check these properties periodically, in the "repeatedly_execute" or "repeatedly_execute_always" functions, so to know when the related events occur.

For example:

function repeatedly_execute_always() {
    if (DragDrop.EvtDragStarted) {
        myLabel.Text = "Started dragging something!";
    } else if (DragDrop.EvtDropped) {
        myLabel.Text = "Something was dropped.";
    }
}

DragDrop steps and basic controls

Now let's discuss automation and manual override.

Previously we mentioned the steps that a drag-n-drop process takes. These steps may be advanced automatically or manually. Automatic execution requires certain parameters to be set for DragDrop, while manual actions require you to call certain functions. Following table allows a quick glance on which properties and functions relate to which steps:

  • Event - is a signal property that tells when DragDrop enters this step.
  • Automation - is a list of options that configure DragDrop's automatic behavior during this step.
  • Control - is a list of functions that allow user to advance from this sto a different step.
Step Event Automation Control
Idle none DefaultHookKey, DefaultHookMouseButton, AutoTrackHookKey HookKeyDown()
Hooking EvtWantObject none HookObject()
Waiting for drag EvtObjectHooked DragMinDistance, DragMinTime HookKeyUp(), Drop(), DropAt(), Revert()
Cancel EvtCancelled none none
Dragging EvtDragStarted AutoTrackHookKey HookKeyUp(), Drop(), DropAt(), Revert()
Dropping EvtWantDrop DefaultUnhookAction Drop(), DropAt(), Revert()
After dropped EvtDropped none none

So, how does this work in practice? Suppose you'd like DragDrop react to certain mouse button, hook and unhook (drop) automatically, and drag Room Objects. Then you set it up like this:

function game_start() {
    DragDrop.Enabled = true;
    DragDrop.DefaultHookMouseButton = eMouseLeft;
    DragDrop.AutoTrackHookKey = true;
    DragDrop.DefaultUnhookAction = eDDUnhookDrop;
}

But because DragDrop knows nothing about types of objects, you have to provide their arbitrary description, and tell when it hooked one. In return, DragDrop will tell you when the dragging is active and where the current object's position is supposed to be; then you may use this information to actually reposition the object. These are the only two things that must be done manually, even if using default hook/unhook actions (and unless you're using DragDropCommon, which already does all that for you):

function repeatedly_execute_always() {
    if (DragDrop.EvtWantObject) {
        Object* obj = Object.GetAtScreenXY(mouse.x, mouse.y);
        if (obj != null) {
            // Hook up this object, under custom mode "1", saving its starting coordinates
            // and object's ID, which may be later used to find out what we are dragging
            DragDrop.HookObject(1, obj.X, obj.Y, obj.ID);
        }
    }
    else if (DragDrop.IsDragging) {
        // remember we saved object's ID in HookObject?
        // we're going to use that now --
        int objid = DragDrop.ObjectTag;
        // DragDrop calculates what the new object position would be,
        // comparing its starting position and relative mouse movement
        object[objid].X = DragDrop.ObjectX;
        object[objid].Y = DragDrop.ObjectY;
    }
}

In the above example everything is automated except of actual object's hookup and repositioning.

Now, let's say you want to keep it like that, but control whether the object is dropped or reverted in realtime. For that you'd have to additionally track EvtWantDrop event and call either Drop() or Revert() before DragDrop does its default action:

function repeatedly_execute_always() {
    //
    // All the previous code goes here
    //
    
    if (DragDrop.EvtWantDrop) {
        // For example, let's make it drop normally only on the walkable area,
        // and revert otherwise.
        if (GetWalkableAreaAt(DragDrop.ObjectX, DragDrop.ObjectY) > 0) {
            DragDrop.Drop(); // let it drop on the new place
        } else {
            DragDrop.Revert(); // return it back to the old place
        }
    } else if (DragDrop.EvtDropped) {
        // Adjust the final position, which will be starting pos if reverted
        int objid = DragDrop.ObjectTag;
        object[objid].X = DragDrop.ObjectX;
        object[objid].Y = DragDrop.ObjectY;
    }
}

Thus, step by step, you can replace automatic behavior for a manual one.

For a more complex scenario, let's say you want to control hooking and unhooking yourself, without using default key action.

function game_start() {
    DragDrop.Enabled = true;
    DragDrop.AutoTrackHookKey = false;
}

function repeatedly_execute_always() {
    if (DragDrop.IsIdle) {
        // Assume we want to use Ctrl + Mouse Click
        if (Mouse.IsButtonDown(eMouseLeft) && IsKeyPressed(eKeyCtrlLeft)) {
            // We don't really have to tell a real key here if AutoTrackHookKey = false;
            // but this information is also saved *for you*
            DragDrop.HookKeyDown(0, eMouseLeft);
        }
    }
    
    //
    // All the previous code goes here
    //
    
    // Additionally we now should test if our keys are still pressed
    if (DragDrop.IsDragging) {
        if (!Mouse.IsButtonDown(eMouseLeft) || !IsKeyPressed(eKeyCtrlLeft)) {
            // Tell DragDrop that the hook key has been released
            DragDrop.HookKeyUp();
        }
    }
}

At this point you may begin to guess, that in the manual mode DragDrop does not care about real keys and therefore your hooking condition could be not related to keys or mouse clicks at all. For this module the "object", the "hook key", and most other things, are merely abstractions. The HookObject may be called for an imaginary object. The HookKeyDown and HookKeyUp may be triggered by certain game events, or by timer, anything really.

The only real thing in DragDrop currently is mouse position, but it's likely to be changed in a future version, to allow to have an abstract cursor position too.

This was the introduction to the DragDrop automation and manual control.

See also:

Using DragDropCommon

While DragDrop automates player input and basic drag events, DragDropCommon automates the real AGS objects handling.

DragDropCommon module is meant for easier dragging of the built-in AGS object types, such as Characters, Room Objects, GUIs and GUI controls, and Inventory Items. Internally this module uses DragDrop module, so you'll have to install one into your game as well. (Please, remember that DragDrop module has to be positioned above DragDropCommon in the list of scripts, otherwise your game won't compile properly.)

See also:

Configuring DragDropCommon is rather simple: you tell it which object types to hook up automatically, and set up few more hooking and dragging options.

First is done using DragDropCommon.ModeEnabled[] array property. In this array the index is enum DragDropCommonMode, which can be one of the following:

  • eDragDropCharacter
  • eDragDropGUI
  • eDragDropGUIControl
  • eDragDropRoomObject
  • eDragDropInvItem

For example: DragDropCommon.ModeEnabled[eDragDropCharacter] = true; enables automatic hookup of Characters, DragDropCommon.ModeEnabled[eDragDropRoomObject] = true; enables hooking of Room Objects, and so on. Technically, enabling a wanted mode is enough, but there are also few things you may want to set.

DragDropCommon.DragMove is the most interesting one. It determines whether an object is dragged on its own, or the module creates a graphical representation of an object to drag around, while object itself stays in place until the drag finishes. The available values are:

  • eDDCmnMoveSelf - drag an actual object;
  • eDDCmnMoveGhostOverlay - create an overlay with a copy of an object's sprite;
  • eDDCmnMoveGhostGUI - use a GUI, where the copy of an object's sprite will be placed as a BackgroundGraphic.

"MoveSelf" mode works with everything except Inventory Items, because items themselves do not have a real position on screen, they are only drawn inside InventoryWindow.

"GhostOverlay" mode works with Characters, Room Objects and Inventory Items (in AGS 3.6.0 and higher). Please note that prior to AGS 3.6.0 overlays were always drawn behind the GUI, which makes them not much useful for dragging inventory items in these older versions. When using this mode you may change the overlay's z-order and transparency with DragDropCommon.GhostZOrder (since AGS 3.6.0) and DragDropCommon.GhostTransparency respectively.

"GhostGUI" mode works with Characters, Room Objects and Inventory Items. You must provide a GUI though, by setting DragDropCommon.GhostGUI. GhostGUI also uses DragDropCommon.GhostZOrder and GhostTransparency properties.

NOTE: GUIs and GUI controls may only be dragged by themselves currently (eDDCmnMoveSelf). The reason is that at the time of making this module AGS did not provide any means to copy a GUI's or control's view onto a sprite. Scripting GUI looks from scratch would be too much for this module.

In addition there are properties DragDropCommon.PixelPerfect and DragDropCommon.TestClickable. First tells that when hooking up an object the click detection should be done using pixel perfect test (works only with Characters and Objects at the moment). Second tells to only hook up Clickable objects if enabled.

Setup example:

function game_start() {
    // we must enable DragDrop, because DragDropCommon depends on it
    DragDrop.Enabled = true;
    DragDrop.DefaultHookMouseButton = eMouseLeft;
    DragDropCommon.ModeEnabled[eDragDropRoomObject] = true; // drag room objects
    DragDropCommon.DragMove = eDDCmnMoveGhostOverlay; // drag using sprite's copy on overlay
    DragDropCommon.PixelPerfect = true; // do pixel perfect click detection
    DragDropCommon.TestClickable = true; // drag only if object's Clickable property is true
}

That is pretty much enough to begin dragging room objects around. However, usually it's not enough for a practical case. You might want to know which objects are dragged and where are they dropped, you might also want to detect some events, and so forth. For that you may use similar techniques as with DragDrop. As DragDropCommon is based on DragDrop, all the events, and ways to override an automatic behavior also apply here. The main difference is that DragDropCommon track the hooked AGS object reference and updates its position for you, so you don't have to bother with that.

For example, if you want to control if the place is suitable for dropping an object (similar to one of the examples in DragDrop's description above), following script would suffice:

function repeatedly_execute_always() {
    if (DragDrop.EvtWantDrop) {
        // Let's make it drop normally only on the walkable area, and revert otherwise.
        if (GetWalkableAreaAt(DragDrop.ObjectX, DragDrop.ObjectY) > 0) {
            DragDrop.Drop(); // let it drop on the new place
        } else {
            DragDrop.Revert(); // return it back to the old place
        }
    }
}

If you would like to do a manual hookup for any reason, DragDropCommon provides a set of functions called TryHookCharacter, TryHookRoomObject and so on, besides simplifying the code a little, they also handle PixelPerfect and TestClickable settings automatically.

Suppose this example: we want to drag Characters with left mouse button and Room Objects with right mouse button.

First of all, we need to have DragDrop.AutoTrackHookKey = false; when setting things up, and then don't enable any DragDropCommon modes at all:

function game_start() {
    // we must enable DragDrop, because DragDropCommon depends on it
    DragDrop.Enabled = true;
    DragDrop.AutoTrackHookKey = false;
    DragDropCommon.DragMove = eDDCmnMoveGhostOverlay; // drag using sprite's copy on overlay
    DragDropCommon.PixelPerfect = true; // do pixel perfect click detection
    DragDropCommon.TestClickable = true; // drag only if object's Clickable property is true
}

Then add new code to repeatedly_execute_always:

function repeatedly_execute_always() {
    if (DragDrop.IsIdle) {
        // Trigger hook on both left and right mouse buttons
        if (Mouse.IsButtonDown(eMouseLeft)) {
            DragDrop.HookKeyDown(0, eMouseLeft);
        } else if (Mouse.IsButtonDown(eMouseRight)) {
            DragDrop.HookKeyDown(0, eMouseRight);
        }
    } else if (DragDrop.EvtWantObject) {
        // Now, we see which hook button was used this time,
        // and try hook certain object type depending on it
        if (DragDrop.CurrentMouseButton == eMouseLeft) {
            DragDropCommon.TryHookCharacter();
        } else if (DragDrop.CurrentMouseButton == eMouseRight) {
            DragDropCommon.TryHookRoomObject();
        }
    }
    
    //
    // All the previous code goes here
    //
}

Finally, DragDropCommon keeps the current hooked object reference (properties _Object, _Character and so forth), so if you ever need to know which object is being dragged at the moment then you may just use one of these.

See also: