Context
This program will control a robot. It is written in Java 7 and compiles into an Android app, but these classes are not Android specific. This program was written for FIRST Tech Challenge and uses some classes from their SDK, available online at https://github.com/ftctechnh/ftc_app.
Purpose
These classes are designed to provide event handling capabilities to the robot so that it can more easily react to user input.
Details
Gamepad.on(ButtonState, Button, EventListener)
is called as the program is initializing and allows the user to set an event listener in the case of an event on a button. There is a loop that runs for as long as the robot is running. Before each iteration of this loop, the member variables of the instances of com.qualcomm.robotcore.hardware.Gamepad
, such as a
and b
(public boolean
), are updated to reflect if some button is currently being pressed. Then, Gamepad.handleEvents()
is called to update the state of the buttons and run user defined event handler code.
Gamepad.java: This is the main class that the user interacts with.
package org.firstinspires.ftc.teamcode.teleop;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Gamepad {
private Map<Button, EventContainer> mButtonEvents = new HashMap<>();
private com.qualcomm.robotcore.hardware.Gamepad mGamepad;
public Gamepad(com.qualcomm.robotcore.hardware.Gamepad gamepad) {
mGamepad = gamepad;
}
public void on(ButtonState event, Button button, EventListener listener) {
if (!mButtonEvents.containsKey(button)) {
mButtonEvents.put(button, new EventContainer());
}
EventContainer eventContainer = mButtonEvents.get(button);
eventContainer.addHandler(event, listener);
}
public void handleEvents() {
Set<Button> buttons = mButtonEvents.keySet();
for (Button button : buttons) {
EventContainer eventContainer = mButtonEvents.get(button);
eventContainer.nextState(button.extract(mGamepad)).handle();
}
}
}
Button.java: This enum contains every button on the physical controller (all are not listed here, but follow the same pattern that those listed do). It also contains the code needed to get if the button is being pressed from the com.qualcomm.robotcore.hardware.Gamepad
instance.
package org.firstinspires.ftc.teamcode.teleop;
import com.qualcomm.robotcore.hardware.Gamepad;
public enum Button {
A(new ExtractButton() {
@Override
public boolean extract(com.qualcomm.robotcore.hardware.Gamepad gamepad) {
return gamepad.a;
}
}),
B(new ExtractButton() {
@Override
public boolean extract(com.qualcomm.robotcore.hardware.Gamepad gamepad) {
return gamepad.b;
}
// More buttons here
});
ExtractButton mExtractor;
Button(ExtractButton extractor) {
mExtractor = extractor;
}
public boolean extract(com.qualcomm.robotcore.hardware.Gamepad gamepad) {
return mExtractor.extract(gamepad);
}
private interface ExtractButton {
boolean extract(com.qualcomm.robotcore.hardware.Gamepad gamepad);
}
}
EventContainer.java: This class implements a state machine and manages adding and calling event handlers.
package org.firstinspires.ftc.teamcode.teleop;
import java.util.HashMap;
import java.util.Map;
public class EventContainer {
private ButtonState mState;
private Map<ButtonState, EventListener> mEventHandlers = new HashMap<>();
private static ButtonState generateNextState(boolean buttonInput, ButtonState oldState) {
switch (oldState) {
case OFF:
if (buttonInput) {
return ButtonState.PRESSED;
} else {
return ButtonState.OFF;
}
case PRESSED:
if (buttonInput) {
return ButtonState.HELD;
} else {
return ButtonState.RELEASED;
}
case HELD:
if (buttonInput) {
return ButtonState.HELD;
} else {
return ButtonState.RELEASED;
}
case RELEASED:
if (buttonInput) {
return ButtonState.PRESSED;
} else {
return ButtonState.OFF;
}
default:
return ButtonState.OFF;
}
}
public EventContainer addHandler(ButtonState event, EventListener listener) {
mEventHandlers.put(event, listener);
return this;
}
public EventContainer handle() {
if (mEventHandlers.containsKey(mState)) {
mEventHandlers.get(mState).onEvent();
}
return this;
}
public EventContainer nextState(boolean buttonInput) {
mState = generateNextState(buttonInput, mState);
return this;
}
}
ButtonState.java: This enum contains the four states that a button can be in.
package org.firstinspires.ftc.teamcode.teleop;
public enum ButtonState {
PRESSED, // Button was just pressed
HELD, // The button has been pressed for more than one iteration
RELEASED, // The button was just released
OFF // The button has been released for more than one iteration
}
EventListener.java: This simple interface is implemented by users and is called when an event occurs.
package org.firstinspires.ftc.teamcode.teleop;
public interface EventListener {
void onEvent();
}
Example usage:
// Initialization
gamepadInstance.on(ButtonState.PRESSED, Button.A, new EventListener() {
@Override
public void onEvent() {
// Somehow react to the 'A' button being pressed
openRobotClaw();
}
});
// ...
// Repeats in a loop as long as the robot runs
gamepadInstance.handleEvents();
My questions
- Is there a better way to handle the extraction of the pressed booleans from instances of
com.qualcomm.robotcore.hardware.Gamepad
? I used the enum method because it ensures that even if a new Button is added, there will still be a way to extract the boolean and without having to update different parts of the code. However, it feels like there is a lot of boilerplate code to perform a simple task. - Are there any ways to simplify my code?
- Is there a better way for users to create event handlers?
- What best practices am I missing here?
1 Answer 1
nice job!
Some hints:
Gamepad.java
- I don't get why do you used same name for
Gamepad
. Would be better to chose another one (CustomGamepad
?) - Both variables can be
final
since you do not expose setter method. This will help performaces. - Since
Button
is an Enum, you can use EnumMap implementation in place of HashMap. This helps performances and allow you to simplify your code
A good tutorial
private Map<Button, EventContainer> mButtonEvents = new EnumMap<Button, EventContainer>(Button.class);
In your constructor, just initialize all
EventContainers
for-each keys ( check same hint forhandleEvents()
)- public void
handleEvents()
can be faster. Just check this explanations
- public void
Since
EnumMap
has all the keys pre-setted, your methodpublic void on(..)
will became smaller since you'll never havenull
public void on(ButtonState event, Button button, EventListener listener) {
// if (button != null)
mButtonEvents.get(button).addHandler(event, listener);
}
Button.java
you simply do not need a private interface and ExtractButton. Simply:
public enum Button {
A {
@Override
public boolean extract(Gamepad gamepad) {
return gamepad.a;
}
},
B{
@Override
public boolean extract(Gamepad gamepad) {
return gamepad.b;
}
// More buttons here
});
public abstract boolean extract(Gamepad gamepad);
}
- of course, if original
GamePad
has only two buttons (bundle-api methods just return a boolean ?!), this kind of infrastructure maybe it's too much but nevermind.
EventContainer.java
with same "EnumMap" approach, you can have a nice Map to generate state simply with gets avoiding all if-else:
private final Map<ButtonState,Map<Boolean,ButtonState>> buttonStateMachine = new EnumMap<etc.> //it's simple if you use a static initializer
and then your method will look like
private static ButtonState generateNextState(boolean buttonInput, ButtonState oldState) {
return buttonStateMachine.get(oldState).get(buttonInput);
}
however you can use directly your Button
object in place of boolean bundle-api object (or avoiding your Button object as said before)
Last, I don't get really how handle() can works in real case scenario but it's more likely a functional issue and not programming one.
-
\$\begingroup\$ Thank you! This helps a lot, especially your advice about the enums. However, your implementation of the state machine includes a Map with a boolean key. A Map feels overkill because there are only two possible values, especially considering that I didn't see anything like BooleanMap. I'm using a HashMap right now, but the overhead seems like it would be way more than I need. Is there some other data type that would be more appropriate? \$\endgroup\$Medude– Medude2018年09月14日 20:19:21 +00:00Commented Sep 14, 2018 at 20:19
-
\$\begingroup\$ I don't think so. Your main map will be an EnumMap, then you need a key/value system. I don't get why the HashMap should be overkilled (look at constructor of HashMap and initialize all in static way), however it's surely possible to place a List<List<ButtonState>> where 0 is false and 1 is true, but to keep your infrastructure you are able to replace Boolean with your Button item when needed and extends easly \$\endgroup\$MrPk– MrPk2018年09月17日 07:22:01 +00:00Commented Sep 17, 2018 at 7:22