COMP 310
Spring 2019

Using Lambdas for Asynchronous Updating and Key Binding with Action Maps

Home  Info  Canvas   Java Resources  Eclipse Resources  Piazza

The following discussion refers to the BallWar demo.

Status Updating Connectivity 

Here's the problem:  There may be many balls generating up information that they wish to have displayed onto the UI, but this could cause the system to be continuously updating various components with the incoming data.  A synchronous update (with respect to an individual ball's needs) of the GUI by actively "pushing" the data out by having each ball set the text of a UI component (via a chain of adapters, etc), would result in the UI getting updated numerous times per repaint cycle, by multiple balls and/or single balls that are updating due to multiple interactions with other balls, for instance, if a score is updated every time a collision occurs. 

Also, if the data is being aggregated into a single display entity, e.g. a label or text area, it becomes impossible for the system to organize the incoming data because it is coming in asynchronously (relative to the overall repaint cyle), in no particular order or timing.      How can this be managed without coupling the entire system together?

One of the key realizations is that the GUI is simply a display system.   In particular, it has no need to constantly maintain a fully accurate rendition of every little thing that happens in the system.   If something happens too fast for a user to see, it doesn't matter if the UI doesn't display it.   That is, the GUI only needs to display the data that appears at the rate at which the GUI is refreshing, not the rate that the data is actually being generated.   This greatly reduces the load and requirements on the GUI in terms of managing the incoming data.   These issues are related to the notions brought up in class a while ago of the model updating at a faster rate than the GUI for increased calculation accuracy.   We will discuss systems that do need to maintain full fidelity of the data coming in when we talk about command queuing later in the semester.

Note:  "model" here refers loosely to all the various components in the model, which includes the central model class plus the balls, strategies, etc.  The discussion deliberately omits the details of the methods that are needed on each class in order to accomplish the communications and other behaviors that are needed within the model itself.

To accomplish the asynchronous reading of the data by the GUI, the model instantiates a lambda whose job it is to read the data being generated.  For instance, this lambda may read the score value of an individual ball kept inside of a scoring update strategy.  The adapter from the view invariantly calls the lambda to obtain the data to display whenever the view needs that information to be refreshed.   The model is free to update the data at whatever schedule it pleases as the view can independently read it.

Asynch data read

Multiple Data Sources by Composing Lambdas

But what if the the model's data consists of multiple pieces of information contributed by decoupled pieces of the model?   That is, it is impossible to a priori aggregate the data before run time?:  For instance, suppose there is more than one ball contributing information, but the number of such balls is not determined until run time. 

One, classic solution is to tie all the contributors to a single, expandable data repository, e.g. an array or vector or database.   This is essentially creating an invariant composite data structure for the lambda to read.   But this has the disadvantage of forcing a coupling of all the participants via the centralized data store.

Another solution is not to aggregate the data, but to aggregate the lambdas.    In much the sense of building a binary tree, a lambda can be created that combines the data returned from two lambdas and then presents (returns) that aggregated data.   (Does this sound familiar?)  So, how must the system be set up such that the controller can invariantly install the adapter from the view where, in the end, the adapter ends up calling a variant lambda?    

Push vs. Pull and Synchronous vs. Asynchronous

You may have noticed the similarities between what is going on here and the call-backs that we have used previously for telling the model when to update.   While both situations use lambdas (commands) that are installed into and called by the view, they are actually quite different.  In the case of the call-backs, the view is invoking a behavior on the model.  We say the view is "pushing" its data (the desire to update) onto the model.    Note that the updating happens at the same time as the painting-induced call to the lambda occurs, i.e. the call to the lambda invokes a synchronous response from the model.   On the other hand, the above updating system has the view calling the lambda to retrieve data that the model has placed at some unknown earlier time.   We say that the view is "pulling" the data from the model, which happens asynchronously with respect to the generation of that data.   

Important:  Do not equate push and pull directly with synchronous and asynchronous.    It is possible to push a command for asynchronous execution for instance (queued lambdas).  Synchronous and asynchronous refer to the relative timing schedules between two processes, while push and pull refer to the direction of data flow with respect to the object that initiates the movement.

 

Keyboard Connectivity using Action Maps

So far, when we wanted something in one part of our system to effect an action in another part of the system, we have used  a quasi-direct connection to do so, where through a series of unequivocal delegations the action is invoked.   The key thing to note here is the lack of any sort of conditionals or choices along the way.   In general, this sort of connection between cause and effect is the most desired, but the implicit requirement for such architectures is the ability to install intelligent objects (here, the lambdas) as the effecting agents.

Unfortunately, the real world is often not so accommodating when one has to interface with other systems or is constrained by other engineering requirements such as speed and/or memory footprint.   In particular, the indications issued by the rest of the system that a particular action is desired is often embodied, not in the form of a behavior execution (e.g. calling the apply method of a lambda) but rather in the form of the passing of a "dumb" identifier value of some sort.

Traditional procedural programming would use an if-else stack or equivalently, a switch statement to branch the program flow based on the identifier value.   This sort of architecture, while small and generally fast, is inflexible and non-extensible.   Systems that require much higher levels of  flexibility and extensibility use a delegation-based approach called "action maps".   Gaming systems are very common place to find action map architectures because of their need to handle a wide variety of arbitrary behaviors.   

Fundamentally, an action map is a mapping that takes an identifier value and maps it to a lambda.   One gives the action map the identifier and the action map returns a lambda which is then executed.  Typically, this is implemented using a hashtable (dictionary).   One can think of an action map as an adapter that converts the dumb identifier value interface into the smarter lambda interface that we used before.

  action map

For situations where there are a large number of options, action maps are actually more efficient because their complexity scales with order 1 while if-else stacks scale as order N.   If the identifiers can be restricted to being consecutive integers, the action map can be further optimized to be an array or a vector, making the dispatching process very fast.

The Java keyboard management system uses an action map architecture, though in a slightly more convoluted 2-step system:

  1. javax.swing.InputMap: This maps a KeyStroke to an arbitrary identifier Object.  All the GUI components we use, particularly the panel upon which the animations are performed have a method to retrieve an InputMap that is used when that component has focus (JComponent.getInputMap). This identifier object can be a String, so an easy and unique identifier to use is the concatenation of the key name  (see the documentation for KeyEvent) and the toString() of the lambda.

  2.   javax.swing.ActionMap: This maps an identifier object to a AbstractAction, which is a type of lambda.  All the GUI components we use, particularly the panel upon which the animations are performed have a method to retrieve an ActionMap that is used when that component has focus (JComponent.getActionMap).    A debatable extra step can be taken to have the AbstractAction delegate to a lambda function, here a java.util.function.Consumer<T> since we want a void return, which insulates the rest of the system from the GUI-specific AbstractAction..

Here is an example of a method on the GUI that associates the pressing of a particular key with a particular lambda, i.e. the given lambda is executed when the given key is pressed (when _canvasPnl has focus).   Note that the keyName is the name of the "virtual key" with the ""VK_" preface (see the documentation for KeyEvent).

	public void addKeyCmd(final String keyName, final Consumer<String> cmd) {
		_canvasPnl.getInputMap().put(KeyStroke.getKeyStroke(keyName), keyName + cmd);  // keyname+cmd simply creates a unique string associated with that keyname and that cmd object.
		_canvasPnl.getActionMap().put(keyName + cmd, (e) -> cmd.accept(keyName));

	}

The BallWar demo utilizes the following classes and interfaces to create the ability to control a ball's velocity using key strokes:

Click diagram for a full-size image.movement system

IMoveable

This interface defines an entity that can be moved about on the screen by defining all the possible distinct movement operations.   The set of operations listed in this example are NOT unique or mandatory--you may decide that there is a better set of operations that can be used that will give better results.   The upon initialization in a ball, a MoveStrategy instantiates an IMoveable object that it then uses in its call to the ball's IBallEnvironment.registerMovementKeys method.   That call will associate the given movement keys (see the IMovementKeys section below) with each operation of the IMoveable, which in turn, controls the host ball's velocity.

IMovementKeys

This interface represents a collection of keys to be used for movement.  The accessors will retrieve the name of the key associated with each operation.    For convenience' sake, a couple of static instances of common key sets were made:  for the arrow keys and the W-A-S-X combinations.  Here the static declaration of the arrow keys set:

  /**
  * A predefined set of movement keys where the arrow keys ("UP", "DOWN", "LEFT" and "RIGHT") are associated
  * with their respective directions and the "END" key is associated with stopping.
  */
 public final static IMovementKeys ARROWSEND = new IMovementKeys() {
    public String getUpKey() { return "UP"; }
    public String getDownKey() { return "DOWN"; }
    public String getLeftKey() { return "LEFT"; }
    public String getRightKey() { return "RIGHT"; }
    public String getStopKey() { return "END"; }
    public String toString() { return "Arrows+End"; }
 };

The MoveStrategy retrieves the currently selected movement key set from the currently selected IPlayer via the IBallEnvironment

 

IBallEnvironment

The ball's environment provides various services for the ball and its strategies.   All communications between the ball and its strategies to the main model should go through the ball environment interface -- do NOT expose the model directly to the ball or strategies!   For instance, to support movement, it provides the ability to associate (register) a set of movement keys with the actions of a moveable object.   The movement key registration method on the IBallEnvironment interface is or is connected to (depending on how you implemented IBallEnvironment ) to some corresponding method on the model itself.

The key registration is performed by going through each key in the set and creating a lambda that will call the associated movement of the moveable object.   The key and the lambda are registered via the IViewCtrlAdapter's (_vCtrlA) which in turn calls the GUI's addKeyCmd method (see above):

public void registerMovementKeys(IMovementKeys keys, final IMoveable m) {
    
    _vCtrlA.addKeyCmd(keys.getLeftKey(), (s) -> m.moveLeft());

    _vCtrlA.addKeyCmd(keys.getRightKey(),(s) -> m.moveRight());

    ...etc...
 }

IPlayer

BallWar was originally designed as a game framework, so it has the notion of "players".   In such, IPlayer is the definition of a single player which encapsulates an associated name, score and set of movement keys.    When a ScoringStrategy or a MoveStrategy is instantiated, it is associated with an IPlayer, which provides the the score value data storage and the associated movement keys for the respective strategies.

Clicking the "Make Player" button on the demo instantiates a player back in the model, associating it with a particular set of movement keys.   The new IPlayer instance is then sent back to the GUI to be placed on the droplist.   The "selected" player is the currently selected player on the drop list.

The initialization of a scoring or movement strategy will cause it to connect with the currently selected player in the model.

 

IViewCtrAdapter

The controller instantiates and connects this adapter so that the model is able to install lambdas associated with keys, get the currently selected player and other properties of the GUI that the model needs.

 

 

 

 

 


© 2019 by Stephen Wong