The Operation Was a Success!


Input for LINK has been completely overhauled, cutting over 200 lines of code and making LINK much more stable and organized.

Interacting with notes was once a tangled mess of if statements, with 5-6 different conditions each, spread throughout the code base and all subtly dependent on one another.

It was a nightmare.

Now, it looks like this:

I was able to use the existing code with very few changes. The most difficult part being, figuring out the proper behavior of the various different type of mouse clicks which I did through simple logging.

I did go over some of this last week, so be sure to check that out if you’re interested, but there was a few differences between what I theorized I could do and what I did in practice.

Step 1: What Are We’re Dealing With?

Mouse collision detection was already in place and completely self-contained so I only needed to grab the information that I already found.

The two pieces of information we get here is whether or not we’re hovering over a note object and if we’re hovering over a resize handle.

Step 2: What is the Mouse Doing?

As mentioned before, I need to know a few different states a mouse button can be in: “hover”, “press”, “double press”, “down”, “activate”, and “release”.

I might go into more detail about this later, but for now, here’s the relevant source code from LINK:

--object_hovered was set by collision detection earlier
local mouse_object = object_hovered

--set mouse state as reported by the engine
local mouse_msg = "hover"
local imc = input.mouse_click
if imc then
    mouse_msg = imc == "pressed" and "press" or "down"
end

local last = input_state.mouse_msg
local active_hold = input_state.active_hold
local active_resize = input_state.active_resize
local active = active_hold == mouse_object and active_hold
local double_click = timers.double_click and input_state.double

--Is mouse within range of original click location?
if double_click then
    --current mouse location based on camera offset
    local ima = input.mouse.absolute
    local mp_x = ima.x - camera.dx
    local mp_y = ima.y - camera.dy

    --original click position
    local isdx, isdy = input_state.double_x, input_state.double_y

    --only check if values are different
    if mp_x ~= isdx or mp_y ~= isdy then
        --the standard distance formula
        local dist = distance(
            input_state.double_x, input_state.double_y,
            mp_x, mp_y
        )
        if dist > click_radius then
            timers:stop("double_click")
            double_click = false
        end
    end
end

if mouse_msg == "press" then
    active = mouse_object
    active_hold = mouse_object
    --resize_object & resize_side from collision detection earlier
    active_resize = resize_object > 0 and resize_side

    if not timers.double_click else
        --first click, set position, enable timer
        local ima = input.mouse.absolute
        local mp_x = ima.x - camera.dx
        local mp_y = ima.y - camera.dy

        double_click = mouse_object
        input_state.double_x = mp_x
        input_state.double_y = mp_y
        timers:set("double_click", theme.double_click_speed)
    else
        --timer still active, so double click is a go!
        if double_click == mouse_object then
            mouse_msg = "double"
        end

        --stop timer so another press doesn't trigger another
        --double click
        timers:stop("double_click")
        double_click = false
    end
elseif mouse_msg == "down" then
    --do nothing
elseif
    mouse_msg == "hover" and
    (last == "down" or last == "press" or last == "double")
then
    mouse_msg = active and "activate" or "release"
elseif mouse_msg == "hover" then
    active = false
    active_hold = false
    active_resize = false
end

-- save in input_state table for later use
input_state.mouse_msg = mouse_msg
input_state.active = active
input_state.active_hold = active_hold
input_state.active_resize = active_resize
input_state.double = double_click

Step 3: What Code Should Run Next?

Once I figure out the mouse state, I run it though the following code:

local istate = input_state.mouse_msg
if istate then
    ------ ![  INITIALIZE  ]! ------

    if istate == "hover" then
        -- ![     HOVER    ]! ------

    elseif istate == "press" or istate == "double" then
        -- ![ PRESS or DOUBLE CLICK ]!

    end
    if     istate == "press" then
        -- ![     PRESS    ]! -------

    elseif istate == "double" then
        -- ![ DOUBLE CLICK ]! -------

    elseif istate == "down" then
        -- ![     DOWN     ]! -------

    elseif istate == "activate" then
        -- ![   ACTIVATE   ]! -------

        istate = "release" --do release next
    end
    if istate == "release" then
        -- ![   RELEASE    ]! -------

    end
    ------ ![  FINALIZE    ]! -------

end

Originally, I thought there would be multiple different blocks of the above code, however, once I started the refactor I realized I was only revamping one part of the entire input system.

Editing notes and the simple dialogs that prompt you when reloading or quitting with unsaved changes are completely separate from this code so I let them be, for now.

Step 4: Update the Program

This is the best part. The entire reason I went through the trouble.

Once we’re done with step 2, I only have to look at one particular condition and execute the code if it is set.

For example, if I double click on an empty area I’ll set input_state.new_note = true in the previous step.

In this step I run the following:

if input_state.new_note then
    input_state.new_note = false
    exit_edit_mode() --stop editing a previous note

    add_new_note(input_state.grab_x, input_state.grab_y)
end

That’s it! No more if this or that or whether a shaman determined it would rain by interpreting the way sand fell onto a piece of paper and only if the moon is in the waxing phase nonsense from before.

For the most part, the underlying logic didn’t change at all.

In fact, in many cases, the code could be simplified quite a bit because I handled mouse movement in Step 3 as well.

And that’s how I shed over 200 lines of code during this refactor.

Fixing the Line Command

The refactor finished much sooner than I had originally anticipated so I took a look at the issues with the “line” command.

Most of them I was able to fix, the remaining issues stem from problems I’ll be tackling soon.

Admittedly, I could have done this before the input system refactor, but the added breathing room I created made it much easier to approach.

In the end, I did what I originally tried (using two notes to represent each node) but I reference the other node by it’s item table (which is consistent within the program’s runtime).

On save, the primary node is updated with the secondary nodes location as part of its note text while the secondary node is discarded. On load, it creates the second node.

Because of using a stand alone note for the secondary node, all existing logic works with no changes necessary.

That was the elegance I was looking for this whole time, but faced with the mess of logic I had been dealing with, it seemed impossible to get working.

What’s Next?

The program is in a much, much more stable state than it’s ever been but there are a few issues that need to be taken care of.

I want to deal with object hierarchy next, which should go a long way in solving my z-ordering issues and will be necessary for the pending UI upgrade.

As I mentioned last time, I already have a proof-of-concept for object hierarchy so it’s just a matter of porting it over to LINK.

I’ll spend the following week working out how I’m going to tackle this problem but with the holidays right around the corner, I doubt I’ll have much to show for it.

Also, there’s a game jam in progress I’d like to submit something to so that’ll slow me down as well.

Files

LINK! version 0.8 Alpha 3 MB
Dec 18, 2021

Get LINK!

Leave a comment

Log in with itch.io to leave a comment.