## TimelineEvents | A Coroutine-Based Event System

babulous
Prole
Posts: 5
Joined: Sun Nov 03, 2019 5:50 pm

### Re: TimelineEvents | A Coroutine-Based Event System

yetneverdone wrote:
Mon Nov 04, 2019 11:19 pm
The example seems complex, could be simplified i guess with more explanation?
I edited the example to include some comments for clarification.
pgimeno wrote:
Tue Nov 05, 2019 11:30 pm
I've just run into one. I'm writing a very straightforward input function that lets you enter characters and delete them. In your example, you create a branch in order to read TextInput, but to me it would be much clearer if I could poll both TextInput and KeyPress with a non-blocking function, in a loop where I would also call E.Step(). As things are, I have to repeat the display code both for the deletion and for the typing branches.
it's cases like these why i created certain events like E.PollMouseActivity. An E.PollKeyboardActivity could probably be useful for something simple like that. I've done some testing with text boxes that have more functionality and my solution always tends to be more branching. Here's my example from GitHub modified, admittedly it gets quite a bit more complex, but can easily be scaled up to include more functionality like; cursor movement, delete key, copy, paste, all without increasing branching complexity much further.

Code: Select all

local E = require "tlevent"

io.output():setvbuf("no")

E.O.Attach()
E.O.Do(function()
print("Hello!")
E.Wait(1)
E.Wait(1)
local name = ""
local enter_name = E.Branch(function() -- wrapping branch
local old_name = ""
E.Branch("loop", function() -- display
print("Enter your name: " .. name)
old_name = name
repeat E.Step() until name ~= old_name
end)
E.Branch("loop", function() -- typing
local text = E.PollTextInput()
name = name .. text
end)
E.Branch("loop", function() -- deleting
E.PollKeyPress("backspace")
name = name:sub(1, -2)
end)
end)
E.PollKeyPress("return")
E.G.Kill(enter_name) -- will kill all the child branches too
print("")
E.Wait(1)
print("Hello " .. name .. ", nice to meet you!")
E.Wait(3)
print("[Press any key to exit]")
E.PollKeyPress()
love.event.push("quit")
end)

Also, I think I just realized what you're meaning when you say "non-blocking events." There are undocumented functions for "Passive" events. It's used internally for E.PollMouseActivity. I didn't document them because most scenarios can also be done with branching, but also I planned on changing how passives work by allowing the creation of timelines with E() inside of timelines instead of using E.Branch. This allows them to be manually updated automatically inside of the timeline. Although I've decided to give the whole thing an overhaul so I guess that doesn't matter much now. In the new version I already have it working to create non branching timelines inside of other timelines.

Here's the source for E.PollMouseActivity.

Code: Select all

function E.PollMouseActivity()
local pmm = E.Passive(E.PollMouseMove)
local pmp = E.Passive(E.PollMousePress)
local pmr = E.Passive(E.PollMouseRelease)
local pmw = E.Passive(E.PollMouseWheel)
while true do
E.Step()
E.PassiveStep(pmm, pmp, pmr, pmw)
if E.IsPassiveDone(pmm) then return "MouseMoved",    E.GetPassiveResults(pmm) end
if E.IsPassiveDone(pmp) then return "MousePressed",  E.GetPassiveResults(pmp) end
if E.IsPassiveDone(pmr) then return "MouseReleased", E.GetPassiveResults(pmr) end
if E.IsPassiveDone(pmw) then return "MouseWheel",    E.GetPassiveResults(pmw) end
end
end

Here's an equivalent in the new version.

Code: Select all

function TL.Event.MouseActivity()
local pmm = TL(TL.Event.MouseMoved)
local pmp = TL(TL.Event.MousePressed)
local pmr = TL(TL.Event.MouseReleased)
local pmw = TL(TL.Event.WheelMoved)
while true do
TL.Event.Step()
pmm:Step(); if pmm:IsDone() then return "MouseMoved",    pmm:GetResults() end
pmp:Step(); if pmp:IsDone() then return "MousePressed",  pmp:GetResults() end
pmr:Step(); if pmr:IsDone() then return "MouseReleased", pmr:GetResults() end
pmw:Step(); if pmw:IsDone() then return "WheelMoved",    pmw:GetResults() end
end
end

This isn't possible in the current version because of some old optimizations in place that prevent garbage collection of timelines unless they explicitly die or are killed. But in the new one they'll clear up even if they just fall out of scope.

pgimeno
Party member
Posts: 1907
Joined: Sun Oct 18, 2015 2:58 pm
Location: Valencia, ES

### Re: TimelineEvents | A Coroutine-Based Event System

To be clear, what I meant by non-blocking is similar to the difference between Channel:demand (blocking) and Channel:pop (non-blocking). The former waits until there's a message; the latter returns nil if there are no messages.

At first I figured I could poll a non-blocking TextInput first, and if there was no text (it returned nil), then poll a non-blocking KeyPressed in order to know if Return or Backspace was pressed. But getting the logic behind that right, is more complicated than it seems, when order is taken into account and multiple keys are allowed per frame. You could, for example, get a keypress for a control key after a textinput key, and think it was not pressed in the textinput event, when in fact it was.

One idea would be to return both keypressed and textinput with the same poll, but it doesn't seem easy. Sadly, keypressed comes before textinput; if it was the other way around, you could store the textinput without passing it down, and on keypressed, send both keypressed and textinput (or nil or false if the key did not generate textinput). Maybe a hack can be done by peeking the LÖVE event queue on keypressed, to see if the next event is the textinput event generated by this key press. Dirty as hell, but that's what we have. And complicated by the fact that on Android, according to my tests, keyreleased comes before textinput too.

With the version still on GitHub, and using the hack I explained in an earlier post, this is a simplified version of how I implemented the input for my template; note drawing is persistent between frames:

Code: Select all

  local str = ''
repeat
erase_str_background(x, y, maxlen + 1)
love.graphics.print(str .. "_", x, y)
local k = TL.PollTextInput()
if k == '\b' and str ~= '' then
str = str:sub(1, -2)
elseif k >= '0' and k <= '9' then
str = str .. k
if #str > maxlen then
str = str:sub(1, maxlen)
end
end
until k == '\r' or k == '\n'
return str

That's clearer to me than using multiple timelines.

### Who is online

Users browsing this forum: No registered users and 5 guests