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.

ShipDemo

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.

LibGDX Project Structure

We have created a web page that describes how an IntelliJ LibGDX project fits together. For the most part you can ignore this – the project is already created for you. But you will need this reference if you have to troubleshoot your project.

LibGDX Tutorials

If you get really stuck, and cannot understand our code, you might want to refer to the official LibGDX tutorials. These are a little spartan, but they are a good place to get started.


Project Files

Download: ShipDemo.zip

While you can use any IDE that you want for this class, we will only support IntelliJ. While most of you have used Eclipse and not IntelliJ, you will find the IDE relatively easy to use. In particular, it is much easier to use LibGDX in IntelliJ than it is to use it in Eclipse. If you really want to use Eclipse, you will need to look at the Eclipse instructions on the LibGDX web site.

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.

intellij-start

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.

The first time you open up the project, IntelliJ will pause to initialize the Gradle system. You are ready to go once you get a pop-up identifying three modules: core, desktop, and gradle.

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 desktop. The core module is where almost all of your source code should go. Specifically, you should pay attention to the package edu.cornell.gdiac.shipdemo.

project-format

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 desktop 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 ones shown below.

launch-config

The very first thing you need to do is to select Use classpath of module. Everything else will give you an error until you do this. Choose desktop_main. Now you can set the main class to DesktopLauncher (use the full name, including the package). You also need to set the Working directory. This is how IntelliJ finds your art assets. For all LibGDX projects, the value is core/assets. Finally, give your configuration a name when you are done. We usually pick the name of the project, as shown above.

Once the launch configuration is set, you can run the application by pressing the play button.


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 one minor exception, you should never need to modify this file. As far as LibGDX is concerned, the root class is GDXRoot.

Th class GDXRoot is an implementation of ApplicationListener It has callback functions for starting the game, running the animation loop, and shutting the game down. However, it does not do the real work of running the game. It only creates three classes, as shown in the dependency graph below.

root-architecture

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 GameMode and LoadingMode are examples of game modes. A mode is just any self-contained way of interacting with the game. A mode can be a menu screen, an inventory screen, combat screen, or whatever. In this game are two modes are the loading screen (LoadingMode) and the game (GameMode).

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. You should only worry about GameMode. As far as you are concerned, this is the true root class of this game.

The Game Mode

All of the other classes are associated with GameMode as their root class. That is, only GameMode 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.

mode-architecture

The illustration above shows the dependency graph for the remaining classes in package shipdemo. We summarize the important classes as follows:

GameMode

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.

GameCanvas

This class handles all of the drawing for the game. Notice that is ignorant of all of the other classes; it only contains raw drawing code. The purpose of this class is to abstract the drawing code for the rest of the application so that it is not quite so low-level.

Most 2D drawing in LibGFDX is done with sprite batches. A sprite batch is a sequence of images to draw (all gathered together as a single mesh, if you have had 4620). Every sprite batch starts with a call to the methods begin and end. Our canvas is designed the same way. The difference is that it is a little more user-friendly than the class SpriteBatch for right now.

This class is a view class. We will see more sophisticated versions of this class in later labs.

Additional Packages

If you look at the package, you will notice a lot of extra packages, all of which contain the prefix edu.cornell.gdiac. These are special classes we have added over the years to fix “shortcomings” in LibGDX. We will talk about some of these much later in class. For now, you can largely ignore these. There are only two additional classes that are relevant to this lab.

edu.cornell.gdiac.utils.Filmstrip

A filmstrip is a sequence of images arranged in a single file. They are read left-to-right, top-to-bottom. Each image is assigned a frame number, starting at 0. By changing the frame number, you can animate your image. Right now, it is used to bank the ship on a hard turn. But you will do more interesting animation later.

edu.cornell.gdiac.assets.AssetDirectory

This class was introduced for the first time in 2021. In the past, this lab would load all of its assets (images and sounds) with hardcoded references. This is incredibly bad design, and it made it difficult for students to adjust to the correct way later. An asset directory is a much more modern approach that we use in the follow-up course CS 4152. The contents of the package edu.cornell.gdiac.assets are all to support an asset directory, but only the class AssetDirectory is relevant for this lab.

An asset directory consists of a json file that specifies all of the assets in the game, together with their settings. For example, if you look at the file assets.json, you will see that you can specify the magFilter or minFilter of an image (these are OpenGL settings that control how smoothly the image resizes). Sounds do not have any additional settings, so they are only specified by name. We will look at this file structure in more detail later, but understand that if you want to add additional images or sounds to your game, this is where you do it.

The class AssetDirectory is the class that you use to access these assets. It is produces by the provided loading screen. 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);

The class SoundEffect is a subclass of Sound that includes a lot of extra features we have added. We will talk about that class much later.


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.

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. When you create the new project, you should type exactly what you see below (except Destintation, which can be anything you want).

troubleshooting

Once you have done that, copy the directories core/assets, core/src and desktop/src into your new project. Edit the configurations and run the new project.

WARNING: Apparently -XstartOnFirstThread (which is required for macOS) causes problems for Windows and Linux. Remove this option on those platforms.


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. As we said above, we create an instance of Lwjgl3Application, initialize it with collection of settings. Add the following assignment to this Lwjgl3ApplicationConfiguration object:

    config.setForegroundFPS(60);

When you do this, there should be no change to the program. Now change the 60.0 to 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). Leave the value as 60.0 when you are done.

2. Modify the size of the game window

The size of the game window is also determined by the Lwjgl3ApplicationConfiguration properties (via the setWindowedMode method). You can also change the size of the window while the game is running by using the setSize() method in GameCanvas. However, we are going to avoid doing this for now.

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.

IMPORTANT: You are welcome to try full screen mode if you like. You need to call the config method setFullscreenMode with a DisplayMode argument like the example below:

    config.setFullscreenMode(config.getDisplayMode());

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 CollisionControlller.

As for drawing, the ships are drawn via the draw(GameCanvas) method in Ship. This method in turn calls draw in GameCanvas twice; first to draw the shadow, and again to draw the ship proper. Read the comments for the appropriate method draw to understand how the method works. This is the method with 9 arguments whose first argument is a TextureRegion (the parent class of FilmStrip). You will know you found it because the comments say

THIS IS THE MASTER DRAW METHOD

Note the special method computeTransform which does all the work. It scales, rotates, and places each image. 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. To make this work better you may want to modify the lifespan of shots, and you should implement a “max speed” for the ships; we recommend changing the “drag” to always affect them even when they are accelerating.

You should also be aware that there are “clipping” issues. You want a ship that is in the process of wrapping around to be drawn partially on one side, and partially on the other, which you can do by drawing the image twice (once on each side of the screen for each screen edge that it is near). 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.

You should not write this code in the Ship class. The clipping code belongs in the draw method for the GameCanvas. Clipping is a property of the window and not the model, so the model should never concern itself with such details. Furthermore, this allows photons to wrap as well. You should not need to add a lot of code to do this. While there are a lot of draw methods in GameCanvas, they all call the master draw method, so this is the only one you need to modify (do not worry about drawOverlay).

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. Make sure a ship that is not in the process of wrapping around the screen does not draw multiple times, however.

5. Play with the draw mode

If you look at GameMode, you will see that we switch to BlendState.Additive when we draw the photons (and the target reticules). Look at this enumeration at the end of the definition of GameCanvas. Try out the other values AlphaBlend 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: The game canvas (and sprite batches in general) is 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: The game canvas (and sprite batches in general) is 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 GameMode. 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, change the scaling to get a feel for how that works. Finally, play with the origin, scaling, screen position, and rotation; get the background to continuously (and slowly!) rotate. Do this by continuously incrementing a value in the draw method of GameMode and then scaling it.

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, but 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 GameMode 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, Feb 01 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:

  • GameMode.java
  • GameCanvas.java
  • CollisionController.java
  • InputController.java
  • PhotonQueue.java
  • Ship.java
  • readme.txt
  • 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