TimelineEvents | A Coroutine-Based Event System

Showcase your libraries, tools and other projects that help your fellow love users.
User avatar
babulous
Prole
Posts: 5
Joined: Sun Nov 03, 2019 5:50 pm

TimelineEvents | A Coroutine-Based Event System

Post by babulous » Sun Nov 03, 2019 6:06 pm

Hello! I'm new here, but I've been playing with LOVE for a few years now. I wanted to share the product of an experiment I've been playing with lately, it's called TimelineEvents.

You can see the readme on github for examples, I'll just talk a little here.

The general idea is to allow the programmer to write code in the order that events happen. The motivation came from having trouble dealing with things that happen over time, especially cutscenes. A lot of the time my code would get really messy or I'd make some weird system that limits me in some way. TimelineEvents is an unobtrusive library that allows the developer to handle events over time without locking you into some system or my way of doing things.

This is a patched up version of a bigger experiment taking the idea further, so the code is a little messy, but it works. If there's bugs definitely let me know!

FYI, this whole thing is being reworked at the moment and the API will change (for the better). Only use this if you want to experiment with it at the moment.

Get it on GitHub!
Last edited by babulous on Tue Nov 05, 2019 1:25 am, edited 1 time in total.

User avatar
zorg
Party member
Posts: 2732
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

Re: TimelineEvents | A Coroutine-Based Event System

Post by zorg » Sun Nov 03, 2019 7:03 pm

Hey, cool library you got there! :3
I do have two observations i wanted to share:
- E.PollMouseActivity() doesn't seem to return the x,y positions in any of the variants it could return them from; this isn't an issue if a love.mouse.getPosition call right after would be guaranteed to return the same values, but i'm not sure if that's the case.
- the "Normal" descriptor for E.G.Status(id), to me, doesn't really tell what's happening with the timeline in question; a better name for it could be thought up; something like "Delegating", since that does tell that it's not that tl, but its branch(es) that are currently running.
Me and my stuff :3True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.

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

Re: TimelineEvents | A Coroutine-Based Event System

Post by babulous » Sun Nov 03, 2019 8:28 pm

zorg wrote:
Sun Nov 03, 2019 7:03 pm
Hey, cool library you got there! :3
I do have two observations i wanted to share:
- E.PollMouseActivity() doesn't seem to return the x,y positions in any of the variants it could return them from; this isn't an issue if a love.mouse.getPosition call right after would be guaranteed to return the same values, but i'm not sure if that's the case.
I think that was my intention, these events weren't meant to mirror love's events exactly. love.mouse.getPosition will return the same values that E.PollMouseActivity would if it did. Internally, E.PollMouseActivity literally just calls the other mouse events. Personally I never really use those values in love's events and call love.mouse.getPosition when I need it, so that's probably why I did it this way. Is there any potential problem in *not* returning the position values?
zorg wrote:
Sun Nov 03, 2019 7:03 pm
- the "Normal" descriptor for E.G.Status(id), to me, doesn't really tell what's happening with the timeline in question; a better name for it could be thought up; something like "Delegating", since that does tell that it's not that tl, but its branch(es) that are currently running.
I agree "Normal" is a little weird, I was naming the statuses after the built in coroutine statuses. In Lua, a coroutine status of "normal" means that a coroutine resumed another coroutine, I saw a parallel and I guess I felt it would be better to use the language built into Lua. I personally don't care what it's called because I don't really refer to that status a lot myself. In fact, there's the function E.G.IsRunning(id) which I tend to use which will check to see if the status is "Normal" or "Running," to me they're identical. But I recognize someone else might find use in it.

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

Re: TimelineEvents | A Coroutine-Based Event System

Post by pgimeno » Mon Nov 04, 2019 2:24 pm

Nice library! I think I should update my template https://love2d.org/forums/viewtopic.php?f=5&t=87262 to use it instead of hump.timer, as hump.timer only provides a sleep function, and I had to use a dirty hack to make it wait for a key or mouse button.

I have some criticisms though. First. I find the naming scheme confusing. O and G are not descriptive. The prefix "Poll" does not make it clear whether it blocks or not, and I'm not sure but there may not be a blocking and a non-blocking variant of e.g. PollKeyPress.

There is also the issue that it's using love.keyboard.isDown and only the first parameter of love.keypressed, but that's only useful for cases in which the programmer wants to use a specific letter, as opposed to a letter in a specific position. Case in point: WASD controls are used by many games for many keyboard types, but French keyboards use ZQSD controls, which are in the same position in an AZERTY keyboard as WASD are in a QWERTY or QWERTZ keyboard. Typically, when you want to use specific letters, you use the textinput event.

I would have liked to have a queue of keys per timeline, in order to not miss when multiple keys are pressed in the same frame. I think it can be presumed that if you don't use keys in one frame that aren't pressed now, they can be cleared from the queue, so that it is kept at a reasonable size.

The scheduling policy for multiple threads (timelines) is not clear from the docs either. I guess it's a simple round-robin policy? i.e. when a thread is interrupted, the next thread immediately executes?

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

Re: TimelineEvents | A Coroutine-Based Event System

Post by babulous » Mon Nov 04, 2019 3:20 pm

pgimeno wrote:
Mon Nov 04, 2019 2:24 pm
I have some criticisms though. First. I find the naming scheme confusing. O and G are not descriptive. The prefix "Poll" does not make it clear whether it blocks or not, and I'm not sure but there may not be a blocking and a non-blocking variant of e.g. PollKeyPress.
I agree, honestly I'm regretting releasing it like this now. For some context, this library is the product of an experiment I did trying to wrap the entire program in "timelines" including graphics, a lot of the questionable design decisions come from pretty quickly patching up that code into something more usable as a library, originally timeline objects were completely hidden and used internally. I'm considering rewriting the whole thing from the ground up with better naming, more thorough, etc. There's a lot of old optimization left over that really limits my ability to design the library, a rewrite is probably for the best. My original experiment would have hundreds of timelines being used created and recycled all the time, so optimization was necessary. But now, I don't think it's that important, I'll want to keep some, but not all. I'm using this for my own projects and I'm kind of uncomfortable with some of this too. Consider this version 0.1. I'm going to keep working on it. I *want* to keep working on it!!

As for "Poll" it's a little hard because of how loose events are in TLE. Yes, "Poll" always blocks. Anything considered an "Event" blocks. And any function is an "Event" if it directly or indirectly makes a call to E.Step(). That function is the function that will actually call coroutine.yield(). I'd like to keep this loose behavior, but I'm not sure how to communicate this all better.

I should add, I'm trying to design this library so the user doesn't need to think or care about whether or not it blocks unless it's specifically important. My vision is to think more in terms of the progression of time, i.e. a line of code represents a movement in time. That's my guiding philosophy for this whole thing.
pgimeno wrote:
Mon Nov 04, 2019 2:24 pm
There is also the issue that it's using love.keyboard.isDown and only the first parameter of love.keypressed, but that's only useful for cases in which the programmer wants to use a specific letter, as opposed to a letter in a specific position. Case in point: WASD controls are used by many games for many keyboard types, but French keyboards use ZQSD controls, which are in the same position in an AZERTY keyboard as WASD are in a QWERTY or QWERTZ keyboard. Typically, when you want to use specific letters, you use the textinput event.
In general the input events are kind of limited right now (no touch events, gamepad, etc.). But that's interesting. How would I check for a key *position* in love?
pgimeno wrote:
Mon Nov 04, 2019 2:24 pm
I would have liked to have a queue of keys per timeline, in order to not miss when multiple keys are pressed in the same frame. I think it can be presumed that if you don't use keys in one frame that aren't pressed now, they can be cleared from the queue, so that it is kept at a reasonable size.
I didn't think about multiple keys being pressed in the same frame. I think this is an easy fix though! I guess this is the silver lining of going public.
pgimeno wrote:
Mon Nov 04, 2019 2:24 pm
The scheduling policy for multiple threads (timelines) is not clear from the docs either. I guess it's a simple round-robin policy? i.e. when a thread is interrupted, the next thread immediately executes?
Yeah I could have communicated that better, but it is actually very particular how it happens.

This depends on a few things. Generally, they run in the order they are created until they are interrupted then move on to the next one. These are coroutines and not real threads after all, so it's still singly threaded. However, if you create a timeline with E(...) then you need to manually update it with E.O.Step(id). From there, in the Timeline's execution, the top level executes first, then it starts executing the branches in the order that they were created. Branches are initialized immediately as they are created, but timelines created with E(...) will wait until the first update. If you create a Timeline with E.O.Do(...) then they update in the order they are inserted and will wait for the next love.update() before initializing.

User avatar
zorg
Party member
Posts: 2732
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

Re: TimelineEvents | A Coroutine-Based Event System

Post by zorg » Mon Nov 04, 2019 4:03 pm

babulous wrote:
Mon Nov 04, 2019 3:20 pm
How would I check for a key *position* in love?
love.keyboard.isScancodeDown
Scancodes are the physical position-based keys, and KeyConstants are what you'd expect those positions to be on a US QWERTY keyboard. Another example for why it's important is that there are games that use the directional arrows + zxcv keys for interaction, and on QWERTZ keyboards, with z and y being swapped... let's just say it's not fun if the games aren't offering remapping since my hand can't really bend that much. :3
Me and my stuff :3True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.

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

Re: TimelineEvents | A Coroutine-Based Event System

Post by pgimeno » Mon Nov 04, 2019 4:04 pm

babulous wrote:
Mon Nov 04, 2019 3:20 pm
Consider this version 0.1. I'm going to keep working on it. I *want* to keep working on it!!

[...] I didn't think about multiple keys being pressed in the same frame. I think this is an easy fix though! I guess this is the silver lining of going public.
Sweet! I'll stay tuned then. (sorry for the out-of-order quoting)

babulous wrote:
Mon Nov 04, 2019 3:20 pm
As for "Poll" it's a little hard because of how loose events are in TLE. Yes, "Poll" always blocks. Anything considered an "Event" blocks. And any function is an "Event" if it directly or indirectly makes a call to E.Step(). That function is the function that will actually call coroutine.yield(). I'd like to keep this loose behavior, but I'm not sure how to communicate this all better.

I should add, I'm trying to design this library so the user doesn't need to think or care about whether or not it blocks unless it's specifically important. My vision is to think more in terms of the progression of time, i.e. a line of code represents a movement in time. That's my guiding philosophy for this whole thing.
Thanks for the clarification. I can see use cases for non-blocking events in a loop, but maybe that's best implemented with a branched timeline. At first sight it sounds like the latter approach could complicate synchronization.

babulous wrote:
Mon Nov 04, 2019 3:20 pm
In general the input events are kind of limited right now (no touch events, gamepad, etc.). But that's interesting. How would I check for a key *position* in love?
Just use the second parameter of love.keypressed and love.keyreleased, and the love.keyboard.isScancodeDown function.

babulous wrote:
Mon Nov 04, 2019 3:20 pm
Yeah I could have communicated that better, but it is actually very particular how it happens. [...]
Thanks for the clarification.

ETA: Could you name some use cases of the library? I can see a few but you probably have a better idea. That can give ideas to people who can potentially benefit from it but don't immediately see a use for it in their programs.

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

Re: TimelineEvents | A Coroutine-Based Event System

Post by babulous » Mon Nov 04, 2019 11:02 pm

zorg wrote:
Mon Nov 04, 2019 4:03 pm
babulous wrote:
Mon Nov 04, 2019 3:20 pm
How would I check for a key *position* in love?
love.keyboard.isScancodeDown
Scancodes are the physical position-based keys, and KeyConstants are what you'd expect those positions to be on a US QWERTY keyboard. Another example for why it's important is that there are games that use the directional arrows + zxcv keys for interaction, and on QWERTZ keyboards, with z and y being swapped... let's just say it's not fun if the games aren't offering remapping since my hand can't really bend that much. :3
Oof, yeah, I don't want that. I'm already rewriting for a 1.0 version with better design that will use scan codes. I've considered including a high level wrapper for input that would listen for multiple input types. i.e. "Jump" control maps to gamepad "A" and key "Z" or it's proper scancode. And you'd listen with E.PollControlPress("Jump").
pgimeno wrote:
Mon Nov 04, 2019 4:04 pm
ETA: Could you name some use cases of the library? I can see a few but you probably have a better idea. That can give ideas to people who can potentially benefit from it but don't immediately see a use for it in their programs.
Sure. Here's a few things off the top of my head.
  • Cutscenes (the original motivation)
  • Complex Animations
  • Sequences of events
  • Code combinations
Personally I've been using it for almost everything that I'd normally do in love.update(). Probably explains why I'm so vague about saying what it's for. To me, this way of thinking is more intuitive. But the primary motivation was cutscenes initially. I'm considering renaming to CutsceneEvents or something, just go full-on promoting it's usage for cutscenes. I'm not good at marketing :?.

Say I want to have some dialog, then a prompt for the player, then the npcs move around or fight or something. Here's an example of what cutscene code might look like.

Code: Select all

-- these functions are made up, MyEvents is a namespace for 
-- hypothetical user-created events in this scenario, everything here is hypothetical.
-- you'll need to use your imagination for some of this. anything NOT in MyEvents or E is
-- NOT an event (does not stop execution)
local cutscene = E(function()
  Game.PauseGameplay() -- take control away from player
  -- MyEvents.Dialog takes a portrait and a message, it will type out the message and wait for the player
  -- to proceed before progressing this cutscene
  MyEvents.Dialog(Baddie.Portrait.Laugh, "Aha! I'm evil, prepare for battle!")
  MyEvents.Dialog(NPC.Portrait.Scared, "Oh no! What ever could I do!?")
  NPC:RunInCircles()
  MyEvents.Dialog(Baddie.Portrait.Laugh, "Mwahahaha")
 
  -- prompt the player with a choice to intervene or not, will not progress until player makes a decision
  local choice = MyEvents.Prompt("Intervene", "Do not")
  if choice == 1 then
    -- move the player entity and then use an event to block the cutscene until it reaches it's destination
    Player:Move(a_little_bit_closer_to_baddie)
    MyEvents.WaitForEntityToStop(Player)
    MyEvents.Dialog(Baddie.Portrait.Smile, "Oh? Do you want to join the fun?")
    -- not events (i.e. do not block), just use your imagination
    Baddie:CastLightning(Player.Position)
    Player:JumpBack()
    MyEvents.Dialog(Baddie.Portrait.Frustrated, "You're fast, aren't you?")
    MyEvents.Dialog(Baddie.Portrait.Smug, "But I bet you won't be fast enough to avoid my special attack!")
    Game.LoadBoss(Baddie)
  else
    MyEvents.Dialog(Baddie.Portrait.Laugh, "It seems I'm unchallenged!")
    MyEvents.Dialog(Baddie.Portrait.Laugh, "Mwahahaha!!")
    Baddie:CastLightning(NPC.Position)
    -- use E.Branch call down 5 lightning and show dialog at the same time
    local kill_npc = E.Branch(function()
      E.Wait(1)
      Baddie:CastLightning(NPC.Position)
      for i=1, 4 do
        E.Wait(.1)
        Baddie:CastLightning(NPC.Position)
      end
      NPC:Die()
    end)
    MyEvents.Dialog(Baddie.Portrait.Laugh, "AAAHAHAHAHA")
    -- make sure both the dialog and kill_npc timeline are both finished before proceeding
    E.WaitForTimeline(kill_npc)
    MyEvents.Dialog(Baddie.Portrait.Neutral, "...")
    MyEvents.Dialog(Baddie.Portrait.Smile, "Very good.")
    MyEvents.FlyOffScreenRealFast(Baddie)
  end
  Game.ResumeGameplay()
end)
Last edited by babulous on Tue Nov 05, 2019 12:12 am, edited 5 times in total.

User avatar
yetneverdone
Party member
Posts: 351
Joined: Sat Sep 24, 2016 11:20 am
Contact:

Re: TimelineEvents | A Coroutine-Based Event System

Post by yetneverdone » Mon Nov 04, 2019 11:19 pm

Cool. I'm looking for a cutscene library as well.

The example seems complex, could be simplified i guess with more explanation?

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

Re: TimelineEvents | A Coroutine-Based Event System

Post by pgimeno » Tue Nov 05, 2019 11:30 pm

pgimeno wrote:
Mon Nov 04, 2019 4:04 pm
Thanks for the clarification. I can see use cases for non-blocking events in a loop,
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.

But thinking about it, it's not too clear how it should work. I see an issue of ordering.

Edit: I've solved it by doing a hack in love.keypressed:

Code: Select all

local keymap = {backspace = '\008', ["return"] = '\013', enter = '\013', delete = '\127'}
function love.keypressed(k, ...)
  E.O.keypressed(k, ...)
  local ti_map = keymap[k]
  if ti_map then
    E.O.textinput(ti_map)
  end
end
Now I can just poll TextInput.

Post Reply

Who is online

Users browsing this forum: No registered users and 4 guests