FinalProject_Checkpoint2
FinalProject_Checkpoint2
Checkpoint Description
Right now, we have a nifty little application that recreates the bouncing DVD logo from years past. The previous checkpoint was all about
dipping our toes into the water, and exploring how to draw simple graphics to the screen.
This little program we have so far is GREAT, but it’s lacking something major that is going to be imperative for our application-
INTERACTIVITY
Checkpoint 2 is going to focus on building out the necessary structure and features needed to let end-users interface with our program.
Our primary goal for this checkpoint is going to be creating interactive BUTTONS.
Objectives
0. Objective 1
1. Objective 2
2. Objective 3
Objective 1
Objective checklist:
• Clean up all of the Velocity and movement code from before
• Create a new rectangle
• Center the rectangle in the middle of our application
All of our Rec2Ds have an argument for Position, so all we should have to do is tell our rectangle to show up in the center of the screen
public void create () {
batch = new SpriteBatch();
_screenSize = new Vector2(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
rectangle = new Rec2D(new Vector2(250, 100), new Vector2(_screenSize.x / 2f, _screenSize.y / 2f), Color.WHITE);
Setting the position of our Rectangle to the half -way point along the x and y axis
Now if we run our code, we should see a white rectangle in the middle of our screen
Right?
In graphics, everything we do is placed onto some kind of coordinate system. To draw something to the screen, we need to give it a
location in that coordinate system.
(100,250)
This point can be thought of as the “starting point” of where we want to begin drawing a Texture from. But which part of the texture is
the point a reference to?
Is (100,250) supposed to be the center point of where we start drawing our Texture?
The Bottom-middle?
Top-right?
Middle-left?
The point in our Texture that we begin drawing from is commonly known as the pivot. In LibGDX, the pivot point of all Textures is set to
the bottom-left corner by default.
The bottom-left of our rectangle is placed exactly at the center of our application
I wouldn’t doubt that there is a way we can change the pivot somehow in our code, but I’d rather everything have the same pivot for
the sake of consistency. So, if we want to center something in our screen, we can simply offset the position of our Rec2D to account for
this.
public void create () {
batch = new SpriteBatch();
_screenSize = new Vector2(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
rectangle = new Rec2D(new Vector2(250, 100), new Vector2(_screenSize.x / 2f, _screenSize.y / 2f), Color.WHITE);
Vector2 rectangleScale = new Vector2(250,100);
rectangle = new Rec2D(
rectangleScale,
new Vector2(_screenSize.x / 2f - rectangleScale.x / 2f, _screenSize.y / 2f - rectangleScale.y / 2f),
Color.WHITE);
}
Offsetting the position of our rectangle by half the size of the rectangle
If we go ahead and run our code now, we should see that our rectangle is now centered!
Objective 2
Checklist:
• Create an InputManager
o Detects and handles user input
• Create CollisionManager
o Calculates if a given coordinate intersects with a rectangle
• Test your code
o Write Singletons for CollisionManager and ImageEditor
• Create a button class
o For all things relating specifically to buttons
• Scale up our code to handle an arbitrary number of Rectangles and Buttons
We COULD implement InputProcessor in ImageEditor.java, but ImageEditor is just the entry-point into our program.
As much as possible, we should try to push additional functionality out to other files. This will help keep our code organized as our
project grows in scale.
Create a new class InputManager.java, then have that class implement InputProcessor. Doing so should kick up an error that you can
resolve by mousing over our class name and clicking Add unimplemented methods
Doing so will add a bunch of new methods to our file. I personally recommend cleaning things up a little, just to save on the vertical
space a bit.
Each of these methods represents some kind of IO Event that can occur in our program. Methods like keyUp and keyDown will be
called whenever the user either presses a key on their keyboard, or lifts up on a pressed key for example. However, they won’t do
anything at all unless we hook this class up to the rest of our program. Head back over to ImageEditor, and add the following lines of
code.
Performing the above changes will cause LibGDX to call the methods inside InputManager whenever an IO event occurs. We can test
this out by adding a print statement to any of our methods inside InputManager
For further reading on the input event system, the following article gives a succinct breakdown on how to use InputProcessor
https://libgdx.com/wiki/input/event-handling
2.1 Collision Detection
Now that we have a rectangle on our screen, and an input system hooked up and running, let’s talk about buttons for a moment. Think
for a moment about what features might belong to a button.
Let’s start with that! Let’s pretend we exist in a naïve world, and our first assumptions are PERFECT. Let’s hop into InputManager and add
some code that will print out “Button has been pressed” when the user clicks the mouse.
LibGDX is system agnostic, meaning your project can run on any platform, even mobile. Because of this, the method that gets called when
the mouse is clicked is called (perhaps against intuition) touchdown
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
System.out.println("Button has been pressed");
return true;
}
public boolean mouseMoved(int screenX, int screenY) {
System.out.println("Mouse moved! " + screenX + " " + screenY);
return true;
}
I’ve moved any methods that have a real body up to the top of my file for better readability
And hurray! Our entire screen has been turned into a button
Let’s imagine that we want each rectangle we’ve drawn to the screen to act as a button instead of treating the entire screen as a
button. Let’s also add the additional complexity of having TWO buttons.
Rec2D rectangle;
Rec2D rectangle2;
public void create () {
Vector2 rectangleScale = new Vector2(100,50);
rectangle = new Rec2D(
rectangleScale,
new Vector2(_screenSize.x / 2f - rectangleScale.x * 2, _screenSize.y / 2f - rectangleScale.y / 2f),
Color.ORANGE);
Two rectangles, equal distances from the left and right sides of the screen respectively
But…I can’t tell when one button has been clicked or the other, or even if no button at all was clicked, when I press down my mouse
button.
In most higher-level graphics libraries, it wouldn’t be a problem to tell which rectangle was clicked on as each of our rectangles
magically knows when it has been clicked on. Unfortunately for us, we have no such magic.
Instead, we can create a system that can DETERMINE which of these two (if either) of these rectangles has been clicked on. And we can
do it with simple measuring and basic math. Let’s start by creating another new class called CollisionManager.java. We’ll be lazy for
now and have the constructor just take two Rec2Ds. We’ll also add a method called getCollision() that will take a Vector2 as an
argument.
This class (getCollision() in particular) will be responsible for telling us if a given coordinate intersects with either of our two Rec2Ds.
This all begs the question though:
Let’s start by breaking things down a little first. This is a situation where our own intuition clouds our ability to solve this problem. Right
now, if we run our program, this is what our eyes can see.
It is very intuitive to tell visually when the mouse is over the top of one of these rectangles, as our mouse will draw over the top of one
of them, blocking or obscuring the rectangle partially. Imagine however, for a moment, the simplest version of how our CPU sees this
scene
Now it becomes dramatically less obvious whether or not the mouse is colliding with our orange rectangle, isn’t it? However, it might
still be possible to apply some kind of spatial reasoning to this problem, so let’s show only the data that our computer can see.
Using just the data present already, is how can we decide if MousePosition here collides with rectangle, rectangle2, or neither?
To start, we can rule out rectangle2 almost immediately. Recall that all of our Textures have their pivot in the bottom-left. If all of our
rectangles have their origin in the bottom-left, and extend right and up, anything to the left of a Rec2D CANNOT possibly intersect.
Note how the blue lines extend up and to the right of our pivot
Even if our green rectangle had an infinitely large width, it would NEVER intersect with our mouse. We can apply this rule to our code
as follows –
return null;
}
However, this code still isn’t complete. Just because our mouse is to the right of a rectangle, that doesn’t mean it intersects. I’ll introduce one more
rule, and leave the rest to you to complete.
Mouse.x is greater than both rectangles’ Position.x, but doesn’t collide with either
So additional processing is still required. We can check to see our mouse is beyond the horizontal bounds of a rectangle with the following check.
We can use this new rule, in conjunction with our previous rule, to determine that the above screenshot doesn’t collide with EITHER rectangle.
There are still two more rules left to determine if a position collides with either of our rectangles. Determine what they are, and apply them to your
getCollision() method.
Hint: They involve the y -axis
Make getCollision() check our FOUR positional rules to determine if coordinates intersects with _recOne, _recTwo, or neither
Alright! If you think you were successful with writing your collision detection code, now all we have to do is hop on over to InputManager, go to
touchDown() and call our getCollision() method to see if we succeeded!
(If you get the point of the following four code snippets below, don’t feel like you need to follow along )
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
getCollision(new Vector2(screenX, screenY));
}
Hmmmm…..
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
CollisionManager.getCollision(new Vector2(screenX, screenY));
}
Maybe….
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
CollisionManager plsWork = new CollisionManager(rectangle, rectangleTwo);
plsWork.getCollision(new Vector2(screenX, screenY));
}
Well now something else is broken…..
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Vector2 _screenSize = new Vector2(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Vector2 rectangleScale = new Vector2(100,50);
Rec2D rectangle = new Rec2D(
rectangleScale,
new Vector2(_screenSize.x / 2f - rectangleScale.x * 2, _screenSize.y / 2f - rectangleScale.y / 2f),
Color.ORANGE);
Rec2D rectangleTwo = new Rec2D(
rectangleScale,
new Vector2(_screenSize.x / 2f + rectangleScale.x, _screenSize.y / 2f - rectangleScale.y / 2f),
Color.GREEN);
CollisionManager plsWork = new CollisionManager(rectangle, rectangleTwo);
plsWork.getCollision(new Vector2(screenX, screenY));
}
Now we’re just getting silly….
What’s going on?! We wrote our code, but now we keep getting all of these errors when we try to something as simple as just calling the method
we JUST made!
This is one of the most common Programming Design issues that we run into when working with larger projects. We create code that we have no
way of being able to run without making our project have to bend over backwards in order to use.
Well, there is one way to fix it that we’ve already done elsewhere in our program. And that would be to simply pass whatever object references we
need to our InputManager during instantiation.
This is perhaps the most intuitive way of solving this problem for many novice/intermediate programmers
This is because now, instead of one reference to our CollisionManager existing in our program (inside ImageEditor), there are now two. Imagine for
a moment that we scale a codebase up a little. If we simply pass items between objects as we desire, we’ll end up with a codebase that could look
something like the following.
Visualization of the pathways through which one object can access another object
Now, this looks a little ugly on first glance, but it gets much worse as you think about how you’re supposed to interact with a codebase like this.
Imagine that you’re writing code inside CollisionManager. You’ve detected a collision with a Button, but you want to check if that Button is set to
active or not before returning it.
This kind of “chaining”, where you have to hop from one object reference to another is what happens when you start passing around and
duplicating data. As we keep creating more and more layers to go through, we create a codebase that becomes increasingly fragile, like a house of
cards.
Also worth mentioning: Note how DriverClass shows up in multiple different files and is given a different variable -name each time, making
for inconsistent access to the same piece of duplicate data.
To cut a long conversation short, there are many who have walked the same path before you. In grappling with these issues for so long, the
software engineering community has built a number of strategies for solving EXACTLY these kinds of problems. These strategies are known as
design patterns.
A design pattern is simply a commonly used technique to solve common design problems in software engineering.
We are going to look at one such design pattern which exists specifically to solve the problem we’ve found ourselves in, the Singleton. If you added
any of the code from the last step, remove it now.
public InputManager(CollisionManager collisionManager) {
CollisionManager = collisionManager;
}
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
CollisionManager.getCollision(new Vector2(screenX, screenY));
System.out.println("Button has been pressed");
return true;
}
Now let’s hop into CollisionManager and edit our code a little to make use of a clever little hack. We are going to add a static instance variable of
type…
CollisionManager
Huh?
Bear with me
And now, when we instantiate our CollisionManager, we’ll set Instance equal to this.
public CollisionManager(Rec2D recOne, Rec2D recTwo) {
_recOne = recOne;
_recTwo = recTwo;
Instance = this;
}
Lightbulbs will be going off soon, I promise
Now, if we want to access some piece of data, or call a method that belongs inside of CollisionManager, we can do it like this.
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
CollisionManager.Instance.getCollision(new Vector2(screenX, screenY));
Your brain should be exploding
This design pattern is called a Singleton because it only works when we plan to have EXACTLY 1 instance of an object. In our case, we don’t need
multiple CollisionManagers, so this works perfectly. We can now call getCollision() as we please.
This is the “other ” piece of functionality for the suffix Manager that I mentioned earlier. Anything we add Manager to, we ’ll also make into
a Singleton
If we create another Singleton in ImageEditor, we can write some code to tell us whether or not we’ve clicked on one rectangle or another.
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Rec2D collision = CollisionManager.Instance.getCollision(new Vector2(screenX, screenY));
if(collision == ImageEditor.Instance.rectangle) System.out.println("Pressed button 1");
else if(collision == ImageEditor.Instance.rectangle2) System.out.println("Pressed button 2");
System.out.println("Button has been pressed");
return true;
}
Drum roll
This is because in LibGDX, the coordinates in InputManager are given as screen coordinates, instead of world coordinates (which everything we’ve
made so far are based in). While we made sure that our screen and program were the same size,
LibGDX says that screen coordinates begin with (0,0) at the top-left
World coordinates begin with (0,0) at the bottom-left
If we invert our y-coordinates, we should finally be good to go! We can do this by subtracting the height of our window by screenY. We can get the
screen height by making _screenSize in ImageEditor public.
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Rec2D collision = CollisionManager.Instance.getCollision(
new Vector2(screenX, ImageEditor.Instance.ScreenSize.y - screenY)
);
And FINALLY! If all went well, we should now be able to click on our Rectangles, and see the result in our console window.
So why did we use this complicated Singleton thing instead of just making all our methods and stuff private?
Fair point actually. A certain reading of what we’ve done might suggest that this would be a lot easier. However, I justify t he decision to
use Singletons on two grounds.
ONE, Java lacks support for a simple way to mark all items in a class as static. The impetus would be on the programmer to simpl y
remember to have to mark all of their code as static . Making a Singleton would allow us to be lazy, and not have to worry about how we
mark our methods or data, and leave them as they are by default (non-static).
TWO sometimes there are classes that have to be instantiated as objects regardless of our desire. ImageEditor for example is not a class
where we can simply mark everything as static, as LibGDX REQUIRES that it not be.
Further reading on singletons and design patterns:
https://refactoring.guru/design-patterns/singleton
Objective 3
Checklist:
• Create a Button class
• Rewire our collision system to use Buttons instead of Rec2Ds
• Rework our code to scale a little better
For our buttons, we want them to carry ALL the functionality of a rectangle, so we are going to inherit from Rec2D.
public class Button extends Rec2D {
public Button(Vector2 scale, Vector2 position, Color recColor) {
super(scale, position, recColor);
}
}
It won’t do much yet, but we can switch the type of rectangle and rectangle2 to Button. We can change their names as well
Rec2D rectangle;
Rec2D rectangle2;
Button button1;
Button button2;
public void create () {
ScreenSize = new Vector2(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Vector2 rectangleScale = new Vector2(100,50);
rectangle = …
rectangle2 = …
button1 = new Button(
rectangleScale,
new Vector2(ScreenSize.x / 2f - rectangleScale.x * 2, ScreenSize.y / 2f - rectangleScale.y / 2f),
Color.ORANGE);
button2 = new Button(
rectangleScale,
new Vector2(ScreenSize.x / 2f + rectangleScale.x, ScreenSize.y / 2f - rectangleScale.y / 2f),
Color.GREEN);
}
Now that we know better, we’re also going to stop passing data around in our program. It’ll break our code for a moment, but while we’re here,
let’s also remove rectangle and rectangle2 from the constructor for CollisionManager
And finally, we’ll switch our render code over to button1 and button2 as well.
Now that we have a Button class and a Rec2D class, ideally, we’d want to differentiate the two by saying that Buttons are clickable and rectangles
are not.
We’re going to do this, as well as make our code more scalable, by adding an array of type Button to our InputManager. If we use the internal Array
datatype from LibGDX, we’ll get a dynamic array that can size itself.
Do NOT use ArrayList. It accomplishes the same task, but operations are much more EXPENSIVE
https://www.baeldung.com/java-generics
Now make our InputManager into a Singleton, and every time a Button is created, add it to this array.
Hint: do the second part in Button’s constructor
InputManager.Instance.Buttons.add(this);
You can call .add() to put an item into an Array
We’re now going to use this array when running collision detection code. Remove ALL references to _recOne and _recTwo.
Inside of getCollision(), loop through InputManager.Buttons, and check to see if coordinates intersects with any buttons. You can loop through and
grab items from an Array like this.
Button iteratingButton;
for(int i = 0; i < InputManager.Instance.Buttons.size; i++) {
iteratingButton = InputManager.Instance.Buttons.get(i);
And the last thing we’ll do in CollisionManager is change the return type of getCollision() to Button instead of Rec2D.
Next, let’s add a method to all of our Buttons called onPressed(). Right now, the code that does stuff when a button is pressed is stuck inside
InputManager. This isn’t a great spot for it, so move this code inside of Button itself, put some dummy print statement inside.
Now, when touchdown() is called inside of InputManager, if the result of the collision is not null (we actually clicked on something), call .onPressed()
on the result button
We also need to change the type of collision to Button for this to work.
If we click on either of our buttons, we should now get a message showing up in our console.
This isn’t terribly interesting or informative though. So lastly, let’s add a static variable to Button that represents how many buttons we’ve made.
private static int _buttonCount;
private int _buttonNumber;
public Button(Vector2 scale, Vector2 position, Color recColor) {
super(scale, position, recColor);
_buttonCount +=1;
_buttonNumber = _buttonCount;
Print out _buttonNumber whenever you click on the button, and you should be able to see the corresponding messages printing to your console
when you click either Button, and nothing should print when you click inside empty space.
Now that we’ve engineered everything, create 4 (100,100) Buttons that are all an even distance away from the center, and one Button right in the
middle of the screen, like the following screenshot. Make sure your collision detection algorithm detects when each square has been clicked on, and
you’re good to go.
Reminder: Go ahead and push all your changes to your Github repository before wrapping up
GREAT WORK!
Things may look far-off still, but we’ve made some amazing progress so far! This checkpoint has been all about building foundational systems. This is
the point where things start getting REALLY exciting now! Everything up to this point has been preparatory work, building the bedrock of our
program. Going forwards, we’re going to be laser focused on building features to make this little graphics program into an image editor we can be
proud of!