Question

During one of my lectures today about Unity, we discussed updating our player position by checking every frame if the user has a button pushed down. Someone said this was inefficient and we should use an event listener instead.

My question is, regardless of the programming language, or situation that it is applied in, how does an event listener work?

My intuition would assume that the event listener constantly checks if the event has been fired, meaning, in my scenario, it would be no different than checking every frame if the event has been fired.

Based on the discussion in class, it seems that event listener works in a different way.

How does an event listener work?

Was it helpful?

Solution

Unlike the polling example you provided (where the button is checked every frame), an event listener does not check if the button is pushed at all. Instead, it gets called when the button is pushed.

Perhaps the term "event listener" is throwing you. This term suggests that the "listener" is actively doing something to listen, when in fact, it's not doing anything at all. The "listener" is merely a function or method that is subscribed to the event. When the event fires, the listener method ("event handler") gets called.

The benefit of the event pattern is that there's no cost until the button is actually pushed. The event can be handled this way without being monitored because it originates from what we call a "hardware interrupt," which briefly preempts the running code to fire the event.

Some UI and game frameworks use something called a "message loop," which queues events for execution at some later (usually short) time period, but you still need a hardware interrupt to get that event into the message loop in the first place.

OTHER TIPS

An event listener akin to an e-mail newsletter subscription (you register yourself to receive updates, whose transmission is later initiated by the sender), rather than endlessly refreshing a web page (where you are the one initiating the transfer of information).

An event system is implemented using event objects, which manage a list of subscribers. Interested objects (called subscribers, listeners, delegates, etc.) can subscribe themselves to be informed of an event by calling a method which subscribes themselves to the event, which causes the event to add them to its list. Whenever the event is fired (terminology can also include: called, triggered, invoked, run, etc.), it calls the appropriate method on each of the subscribers, to inform them of the event, passing along whatever contextual information they need to understand what happened.

The short, unsatisfactory answer is that the application receives a signal (the event) and that the routine is only called at that point.

The longer explanation is a bit more involved.

Where do client events come from?

Each modern application has an inner, usually semi-hidden "event loop" that dispatches events to the correct components that should receive them. For example, a "click" event is sent to the button whose surface is visible at the current mouse coordinates. This is at the simplest level. In reality the OS does a lot of this dispatching as some events and some components will receive messages directly.

Where do application events come from?

Operating systems dispatch events as they happen. They do so reactively by being notified by their own drivers.

How do drivers generate events?

I am not an expert, but for sure some use CPU interrupts: the hardware they control raises a pin on the CPU when new data is available; the CPU fires off the driver which handles the incoming data which eventually generates a (queue of) events to be dispatched and then returns control back to the OS.

So as you see, your application is not really running all the time at all. It's a bunch of procedures that get fired by the OS (sorta) as events happen, but does nothing the rest of the time.


there are notable exceptions e.g. games for once which might do things differently

Terminology

  • event: A type of thing that can happen.

  • event firing: A specific occurrence of an event; an event happening.

  • event listener: Something that looks out for event firings.

  • event handler: Something that occurs when an event listener detects an event firing.

  • event subscriber: A response that the event handler's supposed to call.

These definitions don't depend on implementation, so they can be implemented different ways.

Some of these terms are commonly mistaken for synonyms since there's often not a need for users to distinguish between them.

Common scenarios

  1. Programming-logic events.

    • The event is when some method gets called.

    • An event firing is a particular call to that method.

    • The event listener is a hook in the event method that's called on each event firing that calls the event handler.

    • The event handler calls a collection of event subscribers.

    • The event subscriber(s) perform whatever action(s) the system means to happen in response to the event's occurrence.

  2. External events.

    • The event is an external happening that can be inferred from observables.

    • An event firing is when that external happening can be recognized as having occurred.

    • The event listener somehow detects event firings, often by polling the observable(s), then it calls the event handler upon detecting an event firing.

    • The event handler calls a collection of event subscribers.

    • The event subscriber(s) perform whatever action(s) the system means to happen in response to the event's occurrence.

Polling vs. inserting hooks into the event's firing mechanism

The point made by others is that polling often isn't necessary. This is because event listeners can be implemented by having event firings automatically call the event handler, which is frequently the most efficient way to implement things when the events are system-level occurrences.

By analogy, you don't need to check your mail box for the mail every day if the postal worker knocks on your door and hands the mail directly to you.

However, event listeners can also work by polling. Polling doesn't necessarily need to be checking a specific value or other observable; it can be more complex. But, overall, the point of polling is to infer when some event has occurred such that it can be responded to.

By analogy, you have to check your mail box every day when the postal worker just drops mail in it. You wouldn't have to do this polling work if you could instruct the postal worker to knock on your door, but that's often not a possibility.

Chaining event logic

In many programming languages, you can write an event that's just called when a key on the keyboard is pressed or at a certain time. Though these are external events, you don't need to poll for them. Why?

It's because the operating system is polling for you. For example, Windows checks for stuff like keyboard state changes, and if it detects one, it'll call event subscribers. So, when you subscribe to a keyboard press event, you're actually subscribing to an event that is itself a subscriber to an event that polls.

By analogy, say that you're living an apartment complex and a postal worker drops mail off into a communal mail receipt area. Then, an operating-system-like worker can check for that mail for everyone, delivering mail to the apartments of those who received something. This spares everyone else the trouble of having to poll the mail-receipt area.


My intuition would assume that the event listener constantly checks if the event has been fired, meaning, in my scenario, it would be no different than checking every frame if the event has been fired.

Based on the discussion in class, it seems that event listener works in a different way.

How does an event listener work?

As you've suspected, an event can work through polling. And if an event is somehow related to external happenings, e.g. a keyboard key getting pressed, then polling does have to happen at some point.

It's just also true that events don't necessarily need to involve polling. For example, if the event is when a button gets pressed, then that button's event listener is a method that the GUI framework might call when it determines that a mouse-click hits the button. In this case, polling still had to happen for the mouse click to be detected, but the mouse-listener is a more passive element connected to the primitive polling mechanism through event-chaining.

Update: On low-level hardware polling

It turns out that USB devices and other modern communication protocols have a rather fascinating networking-like set of protocols for interactions, enabling I/O devices including keyboards and mice to engage in ad hoc topologies.

Interestingly, "interrupts" are fairly imperative, synchronous things, so they don't handle ad hoc networking topologies. To fix this, "interrupts" have been generalized into asynchronous high-priority packets called "interrupt transactions" (in the context of USB) or "message-signaled interrupts" (in the context of PCI). This protocol is described in a USB specification:

enter image description here

-"Figure 8-31. Bulk/Control/Interrupt OUT Transaction Host State Machine" in "Universal Serial Bus Specification, Revision 2.0", printed-page-222; PDF-page-250 (2000-04-27)

The gist seems to be that I/O devices and communication components (like USB hubs) basically act like network devices. So, they send messages, which requires polling their ports and such. This alleviates the need for dedicated hardware lines.

Operating systems like Windows seem to handle the polling process itself, e.g. as described in the MSDN documentation for the USB_ENDPOINT_DESCRIPTOR's which describes how to control how often Windows polls a USB host controller for interrupt/isochronous messages:

The bInterval value contains the polling interval for interrupt and isochronous endpoints. For other types of endpoint, this value should be ignored. This value reflects the device's configuration in firmware. Drivers cannot change it.

The polling interval, together with the speed of the device and the type of host controller, determine the frequency with which the driver should initiate an interrupt or an isochronous transfer. The value in bInterval does not represent a fixed amount of time. It is a relative value, and the actual polling frequency will also depend on whether the device and the USB host controller operate at low, full or high speed.

-"USB_ENDPOINT_DESCRIPTOR structure", Hardware Dev Center, Microsoft

Newer monitor connection protocols like DisplayPort seem to do the same:

Multi-Stream Transport (MST)

  • MST (Multi-Stream Transport) added in DisplayPort Ver.1.2

    • Only SST (Single-Stream Transport) was available in Ver.1.1a
  • MST transports multiple A/V streams over a single connector

    • Up to 63 streams; not “Stream per Lane”

      • No synchronicity assumed among those transported streams; one stream may be in a blanking period while others are not
    • A connection-oriented transport

      • Path from a stream source to a target stream sink established via Message Transactions over AUX CHʼs prior to the start of a stream transmission

      • Addition/deletion of a stream without affecting the remaining streams

enter image description here

-Slide #14 from "DisplayPortTM Ver.1.2 Overview" (2010-12-06)

This abstraction allows for some neat features, like running 3 monitors from one connection:

DisplayPort Multi-Stream Transport also allows connecting three or more devices together but in the opposite, less "consumer"-oriented configuration: simultaneously driving multiple displays from a single output port.

-"DisplayPort", Wikipedia

Conceptually, the point to take away from this is that polling mechanisms allow for more generalized serial communications, which is awesome when you want more general functionality. So, the hardware and OS do a lot of polling for the logical system. Then, consumers that subscribe to events can enjoy those details being handled for them by the lower-level system, without having to write their own polling/message-passing protocols.

Ultimately, events like key-presses seem to go through a rather interesting series of events before getting to the software-level's imperative event-firing mechanism.

Pull vs Push

There are two main strategies to check if an event happened, or a specific state is reached. For example, imagine waiting for an important delivery:

  • Pull: every 10 minute, go down to your mailbox and check if it was delivered,
  • Push: tell the delivery guy to call you when they do the delivery.

The pull approach (also called polling) is simpler: you can implement it without any special feature. On the other hand, it's often less efficient since you risk doing extra checks with nothing to show for them.

On the other hand, the push approach is generally more efficient: your code only runs when it has something to do. On the other hand, it requires that a mechanism exists for your to register a listener/observer/callback1.

1 My mailman is typically lacking such a mechanism, unfortunately.

About unity in specific - there is no other way of checking player's input other than polling it every frame. To create an event listener, you would still need an object like "event system" or "event manager" to do the polling, so it would only push the problem to a different class.

Granted, once you have an event manager, you have only one class polling the input every frame, but this doesn't give any obvious performance advantages, since now this class has to iterate over listeners and call them, which, depending on your game design (as in, how many listeners are there and how often the player uses input), might actually be more costly.

Apart from all this, remember the golden rule - premature optimisation is the root of all evil, which is especially true in video games, where often the process of rendering each frame costs so much, that small script optimisations like this are completely insignificant

Unless you have some support in your OS/Framework that handles events like button push or timer overflow or message arrival - you will have to implement this Event Listener patter using polling anyway (somewhere underneath).

But don't get turned away from this design pattern just because you don't have a performance benefit there right away. Here are the reasons why you should use it regardless if you have underlaying support for event handling or not.

  1. The code looks cleaner and more isolated (if implemented correctly, of course)
  2. The code based on event handlers better stand changes (since you normally modify only some of event handlers)
  3. If you happen to move to the platform with underlaying event support - you can reuse your existing event handlers and just get rid of polling code.

Conclusion - you have been lucky to participate in the discussion and learned one alternative to polling. Look for an opportunity to apply this concept in practice and you will appreciate how elegant the code could be.

Most event loops are built above some polling multiplexing primitive provided by the operating system. On Linux, that primitive is often the poll(2) system call (but could be the old select one). In GUI applications, the display server (e.g. Xorg, or Wayland) is communicating (thru a socket(7) or pipe(7)) with your application. Read also about X Window System Protocols and Architecture.

Such polling primitives are efficient; the kernel would in practice wake up your process when some input is done (and some interrupt is handled).

Concretely, your widget toolkit library communicates with your display server, waiting for messages, and dispatch these messages to your widgets. Toolkit libraries like Qt or GTK are quite complex (millions of lines of source code). Your keyboard and mouse are only handled by the display server process (which translates such inputs to event messages sent to client applications).

(I'm simplifying; in fact things are much more complex)

In a purely polling based system, subsystem that might want to know when some particular action happens will need to run some code any time that action might happen. If there are many subsystems that would each need to react within 10ms of some not-necessarily-unique event occurring, they would all need to check at least 100 times/second whether their event had occurred. If those subsystems are in different threads (or worse, processes) processes, that would require switching in every such thread or process 100x/second.

If many of the things applications will watch for are rather similar, it may be more efficient to have a centralized one monitoring subsystem--perhaps table driven--which can watch many things and observe whether any of them have changed. If there are 32 switches, for example, a platform might have a function to read all 32 switches at once into a word, making it possible for the monitor code to check whether any switches have changed between polls and--if not--not worry about what code might be interested in them.

If there are many subsystems that would want notification when something changes, having a dedicated monitoring subsystem notify other subsystems when events occur that they are interested in may be more efficient than having each subsystem poll its own events. Setting up a dedicated monitoring subsystem in cases where nobody is interested in any events, however, would represent a pure waste of resources. If there are a only a few subsystems that are interested in events, the cost of having them watch for the events they're interested in may be less than the cost of setting up a general-purpose dedicated monitoring subsystem, but the break-even point will vary significant between different platforms.

An event listener is like an ear waiting for a message. When the event occurs, the subroutine chosen as event listener works using the event arguments.

There are always two important data: the moment where the event happens and the object where this event occurs. Other argument are more data about what's happened.

The event listener specifies the reaction to that that occurs.

An Event Listener follows the Publish / Subscribe Pattern (as a subscriber)

In its simplest form, a publishing object maintains a list of subscribers' instructions to be carried out when something needs to be published.

It will have a some kind of subscribe(x) method, where x depends on how the event handler is designed to handle the event. When subscribe(x) is called, x is added to the publishers list of subscribers' instructions/references.

The publisher may contain all, some or none of the logic for handling the event. It may simply require references to the subscribers to notify/transform them with its specified logic when the event occurs. It may contain no logic and require subscriber objects (methods/event listeners) that can handle the event. It's most likely to contain a mixture of both.

When an event occurs the publisher will iterate over and execute its logic for each item in its list of subscribers' instructions/references.

No matter how complex an event handler looks, at its core it follows this simple pattern.

Examples

For an event listener example, you supply a method/function/instruction/event listener to the subscribe() method of the event handler. The event handler adds the method to its list of subscriber callbacks. When an event occurs, the event handler iterates over its list and executes each callback.

For a real world example, when you subscribe to the newsletter on Stack Exchange, a reference to your profile will be added to a database table of subscribers. When it's time to publish the newsletter, the reference will be used to populate a template of the newsletter and it will be sent to your email. In this case, x is simply a reference to you, and the publisher has a set of internal instructions used for all subscribers.

Licensed under: CC-BY-SA with attribution
scroll top