Introduction to LibGDX
This week’s lab is an introduction to LibGDX. You will learn how to use LibGDX inside of IntelliJ. You will start off with a sample project called ShipDemo that provides a two-player (hot seat) game. You will modify this ShipDemo sample in various ways to see a few of the things you can do with LibGDX.
This lab is not due during this lab period, but it is to your advantage to work through (and/or learn) as much as you can now, while assistance is immediately available. If you run into problems finishing the lab project later, please feel free to contact a staff member for help (preferably a programming TA).
Table of Contents
Useful References
One of the things that you will learn right away is that there is a lot of instruction missing from this lab. Indeed, a large part of this lab is learning where to look for the information that you need. Of course the TAs can help you. But in addition to the TAs, you will find it very helpful to make use of the wealth of LibGDX material online. The following should be enough for you to complete this lab.
The LibGDX API
This reference is the equivalent of the Java API for LibGDX It details all of the LibGDX classes with their methods. Note that we are using version 1.12.1 of LibGDX this semester
The GDIAC API
We have created several addition Java classes for this course to make things easier for you. In particular, our classes for asset loading and graphics will be used all semester long.
LibGDX Project Structure
We have created a web page that describes how an IntelliJ LibGDX project fits together. While we have already created the project for you, you will need to configure the application.
Project Files
Download: ShipDemo.zip
While you can use any IDE that you want for this class, we will only support IntelliJ. Fortunately, 2110 has moved from Eclipse to IntelliJ, so this should be familiar to you. But the project structure is perhaps a little different than those you worked with in 2110.
Loading the ShipDemo
Download the sample project, unzip it, and put the uncompressed folder where ever you want. It does not have to be in a special IntelliJ folder. Now start up IntelliJ. You should see a start-up screen like the one below.
Select Open. Navigate to the project folder that you want. However, do
not stop at the project folder. Instead, open up the file build.gradle
.
IntelliJ will ask whether you wish to open this as a Project or a File. Choose
Open as Project.
When you open the project in IntelliJ for the first time, the IDE will pause to initialize the Gradle system. You’ll know the setup is complete when a pop-up appears showing three modules: core, desktop, and gradle.
If you encounter a Gradle sync issue or a version dependency mismatch with Java (visible under the Gradle Sync tab in IntelliJ), you can resolve it by following the steps provided here.
Alternatively, for Java dependency issues, go to File → Build, Execution, and Deployment → Gradle JVM and ensure it matches the version of your File → Project Structure → Project SDK. Make sure to consult troubleshooting for any other issues.
Running the ShipDemo
Once you have loaded the Gradle project, you will see two modules in the IDE:
one called core
suffix and the other called lwjgl3
. The core
module is
where almost all of your source code should go. Specifically, you should pay
attention to the package edu.cornell.cis3152.shipdemo
.
However, you cannot run the core
module. None of those classes have a
main
method. Your main
method must be targeted to a specific platform
(e.g. Android, iOS, or Desktop). As this project is designed for the desktop,
this is the purpose of the lwjgl3
module. Inside that project is one single
class: DesktopLauncher
. This has your main
method.
To run your LibGDX project, you need will need to create a launch configuration. See that drop-down menu to the left of the play button in the top right corner? Select it and choose Edit Configurations.
Choose the plus symbol in the top left corner to create a configuration. You will be given a list of configuration types. Choose Application. When you do this, you will see input fields like the one shown below.
If you window does not look like this, then that means gradle did not sync properly. Try resyncing and make sure that there were no error messages.
In filling out this window, you need to choose your version of Java. You also
need to set the classpath of the module. Everything else will give you an
error until you do this. Choose lwjgl3.main. Next you can set the main class
to either DesktopLauncher
or Lwjgl3Launcher
as appropriate. Use the full
name, including the package.
Finally, you need to set the Working directory. This is how IntelliJ finds
your art assets. For all modern LibGDX projects, the value is $ProjectFileDir$
.
When you are done give your configuration a name. We usually pick the name of
the project, as shown below.
Once the launch configuration is set, you can run the application by pressing the play button.
WARNING: While this configuration will run the project on all platforms, it will not allow you to run the debugger on macOS. If you are on macOS and are having problems debugging, see how to add VM options.
Project Structure
To help get familiarized with how to use LibGDX, you are going to make a series of modifications to ShipDemo. But before you do so, you should look through the code and see how it works. There are a lot of classes here (but nothing compared to what you will have by the end of the course). If you are lost, please talk to one of the programming TAs get you back on track.
The Root Classes
As you discovered when you ran the project, the class DesktopLauncher
is the root class. However, with a few minor exceptions,
you should never need to modify this file. As far as LibGDX is concerned, the
root class is GDXRoot
.
Th class GDXRoot
is an child of the class
Game.
On the surface, this class appears to do very little by itself (as we inherit
all of the necessary features). However, you will note that it creates three
important objects, as shown in the dependency graph below.
An arrow from one class to another means that the first class makes a reference to the second class in its code. Throughout this lab you will note that there are no cycles in the dependency graph. That helps keep the code manageable (though this lab still has a lot of room for improvement).
The classes Game Scene
and LoadingScene
are examples of scenes. Most
game applications are composed of a collection of scenes (we will also call
these “player modes” in the first week of class). A scene can be a menu screen,
an inventory screen, combat screen, or whatever. In this game are two scenes are
the loading screen (LoadingScene
) and the game (GameScene
).
The primary responsibility of the root class is therefore to manage these scenes
and switch between them. How do we do that? The root has a method called
exitScene
which each scene calls when it is time to switch to a new scene.
But wait a second. Didn’t we say no cycles? GDXRoot
creates GameScene
but
GameScene
calls a method in GDXRoot
, which is a cycle! Not quite. The
method exitScene
is actually a method in the interface ScreenListener
which
GDXRoot
implements. So GameScene
depends on ScreenListener
, not GDXRoot
.
While this may seem like it is the same thing, it is not. This is an important
concept that we will talk about later in the course.
The Loading Scene
Just as in any game, the loading screen makes sure that all assets are loading before you can continue. There is some fairly sophisticated code in this class. You do not need to worry about this class. We will come back to it later in the course. The only thing you need to know about is AssetDirectory. This is the class that is used to manage assets that are loaded into your game.
How does AssetDirectory
know which files to load? That is the purpose of the
assets.json
file in the assets
directory. That file works like a directory
(hence the name) that lists all of the assets that need to be loaded, as well
as any specific properties we want the assets to have.
To access an asset you use the method getEntry
, specifying the json key of the
image and the LibGDX class corresponding to that asset. So to load the image
ship.png
, you would write
Texture image = directory.getEntry("ship", Texture.class);
On the other hand, to access the laser sound effect, you would write
Sound image = directory.getEntry("laser", SoundEffect.class);
Note that SoundEffect
is a subclass of Sound
that we will talk about
later in the course.
The Game Scene
All of the other classes are associated with GameScene
as their root class.
That is, only GameScene
is responsible for instantiating the other classes
in the game, and not GDXRoot
or DesktopLauncher
. This code follows
a classic Model-View-Controller pattern. This is the primary architecture
pattern for this course. It is ideal for time-based applications that involve
animation or simulation.
The illustration above shows the dependency graph for the remaining classes in
package shipdemo
. We summarize the important classes as follows:
GameScene
As far as this lab is concerned, it is the true root class. It initializes the
game, creating instances of all other classes. It also manages the game-loop
via the update
and draw
methods. If you add new objects to your game,
you will need to modify this class.
This class is a controller class.
InputController
This is a subcontroller for managing player input. It converts the input from low-level commands to something semantically meaningful (e.g. the game verbs). It also hides whether the player is using the keyboard or an X-Box controller.
This class is obviously a controller class.
CollisionController
This is a subcontroller for handling ship-to-ship and ship-to-wall collisions.
Collisions and physics should always be in a dedicate controller. A model class
(like Ship
) should never manage its own collisions. We will see why later in
the course.
This class is also obviously a controller class.
Ship
This class represents a ship. It stores the drawing texture (as a FilmStrip
object) for the ship as well as spatial information like position, velocity,
and rotation. It is a fairly lightweight model. Many of the methods that you
would expect for a Ship object to have are actually managed by the controllers.
Again, we will see why we made this decision later in the course.
This class is a model class.
PhotonQueue
This class is a queue that stores all the active photons. There is a limit to the number of photons that can be on the screen at a given time. If the queue is filled, the oldest photons are removed even if they have not reached their target. As all photons have the same drawing texture, this is managed by the photon queue and not the individual photons.
This class is a primitive example of a memory pool. You want to avoid any heap allocation while the game is running. If you need to add objects to the screen, it is best to preallocate them ahead of time like we do in this class, and then use them as necessary.
This class is a model class.
SpriteBatch
There is no code for this class as it is provided by the GDIAC extensions. This class is what we refer to as the graphics pipeline. It handles all the drawing for the game. Notice that is ignorant of all of the other classes. As it is provided by a third-party library, it would have to be.
We will talk about how to use sprite batches later, but the key thing to
understand is that they have a drawing pass, and all drawing commands
must take place inside the pass. You big the pass by calling begin
and
stop the pass by calling end
. In many cases, the drawing will not actually
happen until end
is called.
This class is a view class.
Troubleshooting
With the proliferation of Java versions, and major changes to the IntelliJ platform, we have discovered that some people have issues loading this project. To prevent conflicts, we have now stripped this project of any IntelliJ settings. That means you must edit the configurations before running the new project.
A common problem that people run into is a failure for Gradle to sync with an error message of
java.lang.UnsupportedClassVersionError: io/github/fourlastor/construo/ConstruoPlugin has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 55.0
This means that a library in LibGDX was compiled in Java 17 and your settings do not support that. We are not sure why this is, as we specifically told the setup program to use Java 11. And interestingly, as long as you use a new version of Java 11, it should work. But regardless, go into Project Structure and choose a more recent version of Java.
If you are still having problems, the best solution is to make a new project on your system, guaranteeing the project uses the correct settings for your computer. In particular, do the following steps
- Create a new LibGDX project
- Copy the contents of
core/src
andlwjgl3/src
into your new project. - Copy the contents of
core/assets
into your new project. - Add the GDIAC extensions
- Edit the launch configuration
When this is done, try running your project.
Lab Instructions
Once you have figured out how the classes fit together, it is time to start making changes to them. We recommend that you make the following changes in order.
1. Play with the FrameRate
While we said that you will almost never need to modify DesktopLauncher
,
there is an important exception: modifying the application settings. This class
creates a GDIAC class called GDXApp
but most of the work is done via an
object of the class GDXAppSettings
created by the method configureApplication
.
This is where you set your game name, your window size, and so on.
Look for this line
config.foregroundFPS = config.getRefreshRate()+1;
This line instructs LibGDX to use the native framerate of your monitor. However, you can set to a fixed value, like this.
config.foregroundFPS = 60;
On many monitors, you will not see a noticeable difference with this setting.
But not try different values such as 100.0, 30.0, 600.0, and 5.0. In the file
readme.txt
which you will submit later, briefly describe what happens (e.g.
the effect these have on the gameplay and/or graphics). When you are done,
restore this line to its original value.
2. Modify the size of the game window
The size of the game window is also determined by the GDXAppSettings
properties, just after the comment about “windowed mode”. The default game
window starts at 1280x720. Try several sizes, including 800x600, 400x300,
100x1000, and 2000x2000, to see what happens. For each size, try flying the
ship around and firing it a bit. In the file readme.txt
, answer the
following questions:
- How do the ships and photons know where to bounce when you change the size?
- Does the game perform worse and/or look worse with some sizes than others?
- Is there a minimum or maximum screen size?
- Is there a “best” width to height ratio? If so, in what way is it best?
Leave the game window set to 1280x720 when done.
OPTIONAL: You are welcome to try full screen mode if you like. You will
need to change the fullscreen
setting as follows.
config.fullscreen = true;
However, do not submit a program in full screen mode for this assignment. Screen sizes are highly unpredictable, and it will interfere with grading.
3. Change the ship sizes
Right now, the ships are the same size. We want you to make them two visibly different sizes. To do this, you will need to change two things: how the ships are drawn, and how they collide with one another.
The latter is actually the easiest part. Read through
checkForCollision(Ship, Ship)
in CollisionControlller
to see how collisions
are detected. You should be able to fix collisions by making a change to the
getDiameter
method *Ship*
. You should not need to update
CollisionController
.
As for drawing, the ships are drawn via the draw(SpriteBatch)
method in Ship
.
You will need to read the documentation for
SpriteBatch
to understand what is going on here. First of all, the ship is drawn twice:
the first time to draw the shadow and the second time to draw the ship proper.
The main difference between the two is the setColor
method. The color tints
the image, turning it into a shadow.
The other important line is computeTransform
. We will talk about these later
in class, but transforms are used to resize and position the ship. Instead of
having you learn the math behind them, the sprite batch class has a static
method computeTransform
that allows you to create them from a set of parameters.
You will see that the shadow and the ship have mostly the same parameters,
except that there positions are slightly different (because the shadow is offset
by a bit).
To change the size of the ship, you change the scale (e.g. the last two
arguments of the draw method). Change the size of the ship by changing these
parameter to something other than DEFAULT_SCALE
. Preferably the value should
depend on your new value of the diameter.
4. Implement wrap-around
Currently, when an object hits the edge of the playfield, it bounces off. If wrap-around is implemented, it should keep going and reappear on the other side. The easiest way to implement wrap-around is to teleport the ship when it goes offscreen. Note that this can cause the ship to speed across the screen (as there is nothing to stop it), so we recommend that you implement a “max speed” for the shots. In particula,r you should change the “drag” so that it always affect a ship, even when it is accelerating.
The problem with teleportation is that it causes clipping. You want a ship that is in the process of wrapping around to be drawn partially on one side, and partially on the other. You do this by drawing the image twice (once on each side of the screen for each screen edge that it is near).
However, we do not want you modifying the Ship
class to do this. A model
class does not know anything about window sizes, and the drawing code is
complicated enough as it is. We just want you to repeat the drawing.
The proper place to do that is GameScene
.
We want you to add a new method to GameScene
with the following header:
private void drawShip(Ship s)
(Remember to add a specification comment to your method). Replace the two calls
to drawShip(SpriteBatch)
in the GameScene.draw
with this method. In that
method, you should draw the ship the appropriate number of times and in the
correct locations. You do this by changing the position of the ship and calling
drawShip(SpriteBatch)
the appropriate number of times. Make sure you get
the “corner cases” right, and make sure the ship’s shadow does not do anything
noticeably weird or different in the transitions. Also, remember to restore
the ship to its correct location when you are done.
Do not just copy-and-paste the drawing code a few times. Instead you should find a pattern and wrap it in a loop, or something like that. In addition, make sure that if a ship is not in the process of wrapping that you only draw it once.
You should do the same thing for the photons, so that they also wrap around
the screen. To keep them from wrapping around forever, you may wish to modify
the lifespan of the shots. This will require you to make a method called
drawPhotons
.
5. Play with the draw mode
If you look at GameScene
, you will see that we switch to BlendMode.ADDITIVE
when we draw the photons (and the target reticules). Look at the
documentation
for this enumeration. Try out the other values like ALPHA_BLEND
and OPAQUE
.
Now look at the file photon.png
in the “assets
folder. Based on what you see,
make a conjecture about what each of these three modes do with the graphics file.
Write your conjecture in readme.txt
. Pick the draw mode you like best and use
that one.
IMPORTANT: Sprite batches are stateful. If you set a blend mode, that blend mode or color will still be set the next time you call draw. This is very confusing for students in this lab. If you change a value, always change it back when you are done.
6. Improve the photon appearance
Even with the change in the blending mode, the photons look pretty boring right now. We can make them more interesting by playing with the color tint. To understand tinting you have to learn a little bit about the Color class works. A color is three values: red, green, blue, and alpha. All these values are between 0 and 1. This means that when you multiply two colors together, you still get a valid color. And that is what tinting does; it multiplies every pixel in the image by the tint color.
This means that if you tint with red, all the white in your image becomes red. If you tint with white, the image is unaffected. If you tint with black, your image becomes completely black. And if you tint with clear (black, but with alpha value 0), your image disappears entirely.
With this in mind, you should also make the shots look better by doing at least one of the following:
- Modify the tinting of the photon so that it changes color continuously.
- Change the shots to fade out as they get older, instead of disappearing immediately .
- Draw each photon a 2nd time with a larger size (try 4x) and lower/faster-fading opacity to simulate glowing.
It is okay to make these drawing changes inside of the code for PhotonQueue. That is because these are properties of the photon and not the world geometry. Whatever changes you make must be visually noticeable. Having code that should work but does not or is too subtle to notice is not acceptable.
IMPORTANT: Sprite batches are stateful. If you set a tint color, that tint color will still be set the next time you call draw. This is very confusing for students in this lab. If you change the color, always change it back to white when you are done.
7. Adjust the sound effects
Currently, every time a Photon is fired, the sound plays from start to finish.
Change the blue ship (but not the red ship) to stop playing its photon firing
sound and starting a new one each time a Photon is fired. To do this, read the
documentation for the methods play
and stop
in the
Sound
class.
8. Modify the background
The background in drawn at the very beginning of the draw
method of
GameScene
. Comment out the line of code that draws the background. Run the
game and describe what happens in the readme.txt
file.
Once you have done that, uncomment the background drawing code to re-enable it. Without changing the texture, change the appearance of the background to something you think looks better or interesting. First, try tinting it a different color.
Next we want you to move the background about a bit: changes its position,
scale, or angle. To that you will need to create a transform, like we do for
the ships and photons, and use the tranform-specific draw
method in
sprite batch. Once you have set this up, get the background to continuously
(and slowly!) rotate. Do this by continuously incrementing the angle in your
computeTransform
method.
A common problem you may encounter with the spinning background is that it spins jerkily or does not seem to spin continuously. If this happens, you probably forgot to cast to float or double before dividing (and so were unintentionally performing integer division). Be sure that the background both looks good and you never see a ‘corner’ of the image. The latter may require you to resize the image when you draw it. However, do not blow it up so much that it is blurry, either.
9. Implement a photon type
Change the red ship (but not the blue ship) to fire a different kind of photon.
Do this by giving the Photon
inner class in PhotonQueue
an extra variable
called “type
”, and changing addPhoton
to take in and assign the photon type
(you may need to modify firePhoton
in GameScene
as well). The red ship
should then be made to add photons of a different “type” than the blue ship does.
Once this is done, change the draw
method in PhotonQueue
to draw the photons
of the blue ship’s type the same way as before, but to draw the photons of the
red ship’s type in a different way. To draw these new Photons, do not use a
different image. Instead, vary some of the parameters, such as the tinting and
scaling of the photons.
In addition to some of the ideas from exercise 6, you could also make some changes in the logic of the photons. For instance, the photons could move faster or slower, or even home in on the enemy ship. Get creative. Be sure that the Photons emitted from the red ship are clearly different from those emitted from the blue ship, and that you go through and test it to make sure everything looks good.
10. Implement collisions for shots
You should make a new method in CollisionController
that checks for
ship-to-photon collisions. Upon detecting a collision, you should make each
connecting photon reflect at a reasonable angle upon colliding, and make it
knock back the ship it hit a little. You will probably find it easiest to copy
(or generalize) the ship-to-ship collision code, and treat photons as having
some arbitrary weight value that gives reasonable results.
You should also make sure that each ship is immune to its own photons. You might find exercise 9 useful for this part.
11. Add mouse support
Make changes to InputController
to add mouse support. Moving the mouse
leftward should turn the ship counter-clockwise, moving the mouse right should
turn the ship clockwise, moving the mouse up should accelerate forward, and
moving the mouse down should accelerate backward. You should also add left-click
to fire photons. Do not remove the keyboard (or X-Box) input from that ship;
mouse and keyboard must both work for it.
Make sure the ship does not respond hypersensitively to mouse movement, but moving the mouse quickly should cause more of a reaction than moving it slowly. Also make sure that you can hold down the mouse button for rapid-fire as it was before. As much as possible, make the mouse control “feel natural” to use, which can be achieved with some tweaking (changes elsewhere to turning speed etc. may be necessary). See the Input and InputProcessor interfaces for information about getting mouse input with LibGDX.
12. Be creative
Finally, make one more change of your choice that you think we will find
impressive, and explain your reasoning in your readme.txt
file.
Submission
Due: Wed, Jan 29 at 11:59 PM
This might seem like a lot of work, but hopefully you will find all of the tasks reasonably simple and straightforward. To turn in the lab, create a zip file containing the files that you have updated/created. At a minimum, that should include the following:
GameScene.java
CollisionController.java
InputController.java
PhotonQueue.java
Ship.java
readme.txt
- An executable JAR of your solution
Even though you provided us with all of the source files, the last item is extremely important. It takes a lot of time for us to import your files and compile them. Therefore, we want something that we can run while looking at your source code. To make this JAR, read the intructions on the main LibGDX resources page.
The file readme.txt
should answer questions and describe anything directly
asked in the above task. In addition it should describe (very briefly) how you
handled each task and where in the code it was handled.
Submit this zip file as lab1code.zip
to CMS