Thursday, May 9, 2024

Incremental Games

 I wrote a post a few years ago about the essence of a game. The basic gist of it was hypothesizing about what makes a game a game, and more importantly, what makes it enjoyable. Basically, in answer to my own question, we have seen mobile game companies leverage certain game mechanics, which might not exactly be dark patterns but are at least closely related. Basically, we know that people respond positively to certain types of stimulus, and so we can design games to provide this stimulus and keep the dopamine running, which makes people continue to play.

The mobile gaming scene, of course, uses this in nefarious ways to get people to spend money on what I consider to be "dumb stuff". I try to be nonjudgmental about others' choices, but when it comes to these specific types of microtransactions in mobile games, it is really hard to see the positive benefits that the purchaser is receiving. Still, I don't want to ruin anyone else's good time, and I am sure it would be easy to judge the way I spend my money (mostly scratch off tickets and dinners at Medieval Times), so let's instead look critically at the mobile game developers and the way they are tricking people into spending money.

Anyway, the reason that I bring this up is that one of my favorite game genres is incremental games, which rely on many of these same dopamine producing tricks to make button clicking and number growing into a game. The main difference here is that in general the incremental game community, at least the part centered around the r/incremental_games subreddit and to a more distributed extent, the galaxy.click website is firmly against microtransactions, forced ads and dark patterns in games. Basically, while the underlying mechanics might be similar, the intentions are pure in the latter case, and are manipulative in the former. 

What is interesting, though, is that for an at least moderately popular subgenre of gaming there is very little written about it. I am going to make a big assumption and say that games that are not monetized provide little incentive to media outlets to provide coverage as there is no obvious financial benefit. Imagine me yelling capitalism while doing jazz hands right now. 

Anyway, I have been thinking of writing about the incremental games in some detail for a while now, and over the upcoming weeks I think I will try to put bits and pieces together to come up with a coherent history and explanation of the genre. Happy clicking! :)



Thursday, April 25, 2024

Remainder Reminder

 Well, I got my homepage into a position that I like, and rolled it out. It has a cool kind of old-style terminal vibe that I like (click and take a look, I'll wait!). I think it also looks cool on mobile, where I moved the sections to tabs on the side and still holds most of the same old terminal vibes without the exact same form factor. I also made it navigable via keyboard (as well as mouse/touch) to really give it that terminal feeling. The problem is, I have no content, haha. I actually just would like to use it for a kind of landing page that shows various projects I am working on, but that kind of gets weird because of my diversity of interests. I still need to think about how to represent various different types of projects in a way that is at least somewhat intuitive or at least makes sense. We'll see how that goes.

A couple days ago I was writing about my old blogs and how they just disappeared into the ether, which had me thinking about it just as I was going to bed and a lightbulb popped over my head. Wayback Machine! So rather than going to bed at my usual early hour, I crawled through a bunch of snapshots and uncovered some old gems. Well, gems for my nostalgia, I don't think anyone really cares about me playing FFXI or drinking to excess for a frankly disturbing number of days in a row. And just looking through now, I found some writing that is fairly transphobic*, even though it is couched language expressing that I don't care how other people live, which was I guess fairly tolerant for 2005, but in 2024 just isn't cutting it. So, perhaps fortunately, my blog wasn't popular enough to be indexed too often, and there are only bits and pieces available to read. Still, I am considering trying to dredge through and maybe repost anything interesting I come across (if there is anything like that) for curiosity value.

In other news, I have been training for a half marathon. Before COVID I was a pretty competitive runner, and would generally finish in the top 10-20 participants in any given half marathon. Then COVID hit and I stopped exercising. There was an outdoor mask rule that people took seriously, and it is hard to do cardio with a mask on, so I basically just gave up. Fast forward to about a year ago (April/May 2023) and my work sponsored people to join the Incheon Half Marathon. I signed up for the 10k race because I knew I was in terrible shape, and ended up having to run walk to a 56:53 finish (I just looked it up). That is a shameful pace and I felt terrible about myself. I realized that I needed to get my act together and start doing something because this wasn't good.

I started running daily again after that wake-up call and pushed and pushed most of last year. My pre-COVID half marathon time was between 85-88 minutes, and I really wanted to get back to that level, nevermind that I am five years older now (45!!! almost 46!!!). I didn't manage to break the 90 minute barrier last year (I ran something like 90:20 in November), but I was right there, and felt confident entering 2024 that I would be in a good place come Spring. Also, there is a certain poetry behind the idea that I went from run walking a 10k at the 2023 Incheon Half Marathon to being back on my game and running under 90 minutes in the half course at the 2024 race. 

Sadly, that was not to be. The original date that registration closed was like April 22, with the caveat that there was only room for 10,000 runners. The race never filled up in the past, so I waited. When I checked it on April 6th, just out of curiosity, the website had a pop up that said they reached the quota and registration would end on April 5th at 5:00pm. I missed the deadline by like 12 hours. This was frustrating on multiple levels. First, I really like the Incheon Half Marathon. It starts at Munhak Stadium and then goes straight down the road through some underpasses, over some overpasses, then over the Songdo Campustown Bridge and all the way down through Songdo to the sea before looping back and ending at the stadium. It is a beautiful run, and it is in my neighborhood. Also, it is a big race and they have some international talent come to compete for prize money. The second reason I got frustrated was because I had planned my training specifically for the date of May 12, and now had to scramble to find another race. One annoyance of living in Songdo is that it is surrounded by the sea and even though it is in the 'metro area', getting to races in Seoul or other places is too much hassle. 

Anyway, I looked at the race calendar and weighed my options. There was a small race in Sangam (Seoul World Cup Stadium) on the same day, and then another race on May 25 in Incheon at Seaside Park on Yeongjongdo (where the airport is). I weighed my options, small uncool race in an inconvenient place but on the correct date, or wait two weeks and run a more fun race closer to home. Well, the deciding factors were that I was training for a specific date and that May 12 is a Sunday, while May 25 is a Saturday. Sundays are easier for me to manage with family obligations and such, and so I chose the smaller less convenient race. Plus, the real scale-tipper was that May 12 in the morning will probably have tolerable weather, while May 25 is getting really close to the blast furnace zone of Korean summer. Either way, Game On.

*I am embarrassed and appalled that I would ever write something like this, and even though probably no one read it then and no one will read this now, I am sorry that I said such gross things. That being said, it makes me feel good to know that I've grown as a person enough that I see previous writing and am like ewwww, I cannot believe that I wrote that. 

Tuesday, April 23, 2024

It's Been a While

Snail
Hard Truths: I started blogging back in like 2001-2002 on Xanga of all places. I started as a Lineage 1 blog and detailed my exploits traveling around with my trusty canine companion, which mostly involved being murdered in upsetting ways in what was, I think, the least forgiving PVP game ever. From there, I moved around a bit and started self hosting. I came to Korea in 2004 and then around 2010, I got married and my life went in a different direction. I stopped paying my hosting bills and lost those six years of content, and Xanga obviously stopped existing long, long ago, which means that about a decade of my writing went poof. To be honest, I kind of like the ephemerality of it all, and while my Lineage blog might fulfil some nostalgic yearning, it isn't exactly the works of Shakespeare, or even Dan Brown for that matter. My early years in Korea might have had some curious value, but the world changes and it was mostly stuff written about places that no longer exist and friends that I no longer meet, and so it pretty much falls into the same basket. 

 Anyway, for the past fifteen years or so I have been wanting to start a blog again. It isn't fashionable, and I don't ever expect anyone to read it, but I find the concept of putting my ideas into words cathartic. However, I've changed a lot in this long time. I got a 'serious' job, I have kids, I am almost through a grueling PhD program, and so on and so forth. BUT! I was talking to some students this morning and happened to be talking about planning for the future and mentioned that it is a good idea to do some self-reflection before making a plan. Think about who you are and who you want to be, and then try to make the necessary moves to put yourself in a position where you can be someone who you like. I know that when I was younger I just made decisions based on what I thought would put me into a better position relative to my peers without ever actually considering what position I actually wanted to be in. It took me a long time to realize that, for instance, chasing money or status at work is not the only way to progress along the path of life, and this is something I wish I thought about earlier. Actually, when I first came into my current job, some people I work with were here for ideological rather than financial reasons. Like they wanted to help people and do the right thing and make the world better, and weren't here just for the paycheck. It took me a long time to come to terms with the fact that the way I was looking at life might not be the best way to do so. Honestly, it took me a long time to even realize that it might not be the only way to do so. Still, once I made that leap, then I realized that I should be focusing on the things that make me who I want to be, and not the things that I think will raise me in the esteem of others. That being said, it turns out that these are often the same things! In my blinkered past, I could only imagine people being judged based on income or job title and never thought of people being judged by the quality of life they lived. Or, I never imagined that there were other ways to judge quality of life than through financial and career success. 

 I know, I know, there are tons of after school specials that explain this stuff, but for whatever reason it took me four decades before I finally started picking up what they were putting down. All of which is to say that I have spent a lot of my recent years checking boxes off of someone else's bucket list without ever thinking about if it is what I am supposed to be doing. On a macro scale, my life is moving in the right direction, my kids are healthy, I have a good job that I think I am good at, I am finishing a terminal degree in a subject that I am strongly interested in, and I think that I am working toward making the world a better place. On a micro level, though, a lot of this building towards the future caused me to cast aside things that I enjoyed but couldn't quantify the value of, if that makes sense. Of course, blaming all of this on a lack of self reflection is kind of ignoring the fact that there are lots of things we do as adults because we have to, and I didn't really have much choice in my lack of free time due to work and family. Still, my kids are young but getting a bit more independent, I am fairly entrenched at work, and this should be my last semester as a student (knock on wood). All of this means that I need to start reprioritizing and figuring out what it is I want to be doing, rather than focusing on what I have to be doing. 

Chief among the things that I would like to start reinvesting my time into are coding and blogging. Neither are related to my job or anything like that, but I enjoy both while being good at neither. So all this navel gazing and rambling is basically to say that I am hoping that I will start both blogging and coding with some regularity going forward. Indeed, I just published a fifteen minute incremental game called Snail Facts! (go check it out, I'll wait), and am working on a homepage redesign to reflect my current style sensibilities. I finished the desktop version, more or less, but now I need to figure out how to translate that aesthetic to work on a phone screen. Still thinking about it. Anyway, until later, yo.

Wednesday, January 2, 2019

Check us out on itch.io!

Happy New Year! We have started porting our Android games over to itch.io so that they can be played on any device, so head on over to our page there and check out what we have to offer. Right now it is just one game, but in the next couple of days we should have a few more up.

In the meantime, let me tell you about Rodney Dangerfield. This isn't a wild tangent, I promise. You see, Rodney Dangerfield had always wanted to be a stand up comedian, but he suffered from a simple problem. He wasn't funny. No one liked his jokes, he was booed off stage, he was a failure. Most people would quit at this point (I know I did, but that story is a wild tangent, so it can wait), and in fact Rodney did quit comedy for a while. The problem was that he had an itch, and it wouldn't go away. So, he ended up quitting his job, destroying his marriage, and almost ruining his life to find out why he wasn't funny and to use that information to fix the problems and become a successful stand up.

To go about this, he went on an epic quest (of sorts) to find out what exactly was funny. He wanted to know what exactly it was that made people laugh. In effect, he was looking for the essence of comedy. He wanted to strip away all the cruft and find the simplest form of humor, figuring that from there he would be able to master the medium. Well, he did in fact. He removed all the excess layers of story and exposition that other comics relied on and became the king of one liners. He found the root of the joke and used that to become very successful.

So what does that have to do with us? Well, I am of a certain age, and I started coding as a hobby a year or two ago because I felt that my needs weren't being met by software developers. Let's be honest, casual games suck, microtransactions and pay to win sucks, huge AAA cinematic gaming experiences aren't fun anymore, and mobile gaming is a cess pit. I realize that some of this is age, that everything was more important, shinier in my youth, but I also really believe that there are things that make the games of yore more appealing.

First, the unlimited resources of today - both computational and financial - allow game creators to polish turds so to speak. With enough computer power any game can be beautiful no matter how stupid the underlying gameplay is. With enough financial power, studios can and do force us to pay attention to these games that we would otherwise ignore. I know a lot of people will disagree with me, but I don't personally care about the horses' testicles in Red Dead Redemption. What is the gameplay like? Is there actually a game under there? As a counter example, check out this essay of the perfection that is the beginning of the original Super Mario Brothers game for the NES. Now we joke about how a game will have a ten hour tutorial before you get started.

Next, there is the myth that creating a mobile game will make you wealthy beyond belief. Hey, look at Flappy Bird or 2048! They are so simple, anyone can do it. This leads to poorly made games filled with ads to the point that even the poor gameplay is choked out. Now, I am not against having ads in apps, in fact most of our apps do contain ads, but the user experience has to come first.

Finally, microtransactions and play to win are the worst ideas ever. Forcing a player to pay money to get more 'turns' for the game, or making important items only available in the cash shop is a great way to ruin a game.

The truth is that, to me, games have lost much of their appeal. Don't get me wrong. I still play Pixel Dungeon daily, and since I reviewed the game in early 2013, that means I have been playing it for five years. The problem, though, is that most games have lost that thread of what a game really is.

So, like Rodney Dangerfield, we are looking to trim all the fat from the gaming experience and see what is left. Looking at timeless games like Tetris show us that a game doesn't need shiny graphics or even a story, they just have to be fun. So, what then is fun? Well, let's find out together.

Friday, July 20, 2018

Basic Sprite Animations in libGDX

In the last post we discussed how to use the texture packer to create sprites. And now we will take a look into how to put these sprites into a libGDX application.

First things first, you need to copy the files you create with the texture packer into the app's asset folder. These assets are typically stored in the Android folder of the app, so in the case of Dot's Dots, the directory is something like this:

/home/netbook/AndroidStudioProjects/dotsdots/android/assets


Don't forget there are at least two files that need to be copied. The PNG file(s), which are the images, and the ATLAS file, which is a map of the images.

Now that the image files are in place, let's look at the code used to animate the sprite.
package com.petenotpete.dotsdots;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;

public class DotsDots extends ApplicationAdapter {
private SpriteBatch batch;
private TextureAtlas textureAtlas;
private Animation<TextureRegion> animation;
private float elapsedTime = 0;

@Override
public void create () {
batch = new SpriteBatch();
textureAtlas = new TextureAtlas(Gdx.files.internal("dotsprite.atlas"));
animation = new Animation<TextureRegion>(1/60f, textureAtlas.getRegions());
}

@Override
public void render () {
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

batch.begin();
elapsedTime += Gdx.graphics.getDeltaTime();
batch.draw(animation.getKeyFrame(elapsedTime, true), 0, 0);
batch.end();
}

@Override
public void dispose () {
batch.dispose();
textureAtlas.dispose();
}
}
Now let's look at the code. The magic here is this line:
animation = new Animation(1/60f, textureAtlas.getRegions());
This sets the animation speed (60fps in this case) and the sprite frames from the texture atlas. The next key part are these lines:
elapsedTime += Gdx.graphics.getDeltaTime();
batch.draw(animation.getKeyFrame(elapsedTime, true), 0, 0);
The elapsed time lets the animation know what frame should be displayed, and the 'true' following it shows that the animation should loop. So in this animation, it uses all the sprites on the sprite sheet, but oftentimes we will have more than one image or animation on a sprite sheet and will need to differentiate between them. If you use the libGDX texture packer to create the sprites, the sprite atlas names the frames after each image's individual filename. The best way to use this to your advantage is to add a prefix to each different image in a spritesheet and number them sequentially (like: walk001.png, walk002.png, walk003.png, run001.png, run002.png, run003.png, etc.). In the code you can separate the animations as follows:
public void create() {
batch = new SpriteBatch();
textureAtlas = new TextureAtlas(Gdx.files.internal("dotsprite.atlas"));

TextureRegion[] walkFrames = new TextureRegion[10];

walkFrames[0] = (textureAtlas.findRegion("0001"));
walkFrames[1] = (textureAtlas.findRegion("0002"));
walkFrames[2] = (textureAtlas.findRegion("0003"));
walkFrames[3] = (textureAtlas.findRegion("0004"));
walkFrames[4] = (textureAtlas.findRegion("0005"));
walkFrames[5] = (textureAtlas.findRegion("0006"));
walkFrames[6] = (textureAtlas.findRegion("0007"));
walkFrames[7] = (textureAtlas.findRegion("0008"));
walkFrames[8] = (textureAtlas.findRegion("0009"));
walkFrames[9] = (textureAtlas.findRegion("0010"));

walkAnimation = new Animation(1/60f, walkFrames);

//or you can just pass the frames directly

runAnimation = new Animation(0.1f,
(textureAtlas.findRegion("0011")),
(textureAtlas.findRegion("0012")),
(textureAtlas.findRegion("0013")),
(textureAtlas.findRegion("0014")),
(textureAtlas.findRegion("0015")),
(textureAtlas.findRegion("0016")),
(textureAtlas.findRegion("0017")),
(textureAtlas.findRegion("0018")),
(textureAtlas.findRegion("0019")),
(textureAtlas.findRegion("0020")));
}
And there you go, you have a sprite moving around.

Tuesday, May 22, 2018

Creating Sprites with libGDX texturepacker

One of the differences between using libGDX and and not using a game engine is that libGDX uses sprite sheets. This is good, because in OpenGL, binding textures is fairly resource expensive and it is better to bind one large resource once than many small resources.

There are plenty of tutorials online about creating sprites, and I am certainly not much of an artist, so I will leave that to others to explain. But, I will say this, if you are like me and end up with a GIF file or have each of your individual sprites saved as layers in a single image there is a trick to getting them out without saving individually.

To separate layers of an image file into separate images the easy way, save the image file as an OpenRaster file (.ora extension). GIMP can do this for free, and likely most other image editing software can as well. After saving, open the .ora file using compression software (7zip or WinRAR), open the 'data' folder, and you will see each individual layer saved as a separate .png file. Easy.

Otherwise, it is important that each of your animation frames is in numerical (or alphabetical) order so that the sprite sheet will be created in the proper order. In my case, I number each frame like 001.png, 002.png, 003.png and so on.

For this tutorial, I made a simple 12 frame animation of a dot running (the app I am making is called Dot's Dots*, right?). You can see it in GIF form below, but I won't provide the images or the sprite sheet for you, because I think it is important that you try this for yourself. And honestly, you don't even need to make it 12 frames, you could have a 3-4 frame animation of a stick man walking and it will be fine.



Now, hopefully you created a simple animation of a few frames. In order to create your sprite sheet the first thing you should do is download the texture packer JAR file. Save it somewhere convenient, and use the following command at the command line to create the sprite sheet:

// OS X / Linux java -cp runnable-texturepacker.jar com.badlogic.gdx.tools.texturepacker.TexturePacker [inputDir] [outputDir] [packFileName]

// WINDOWS java -cp runnable-texturepacker.jar com.badlogic.gdx.tools.texturepacker.TexturePacker [inputDir] [outputDir] [packFileName]

[inputDir] is where you saved your individual image files, [outputDir] is where you would like the sprite sheet to be saved and [packFileName] is going to be the name of the output file.

In my case, I want to take my image files and save the sprite sheet directly into my apps assets folder. Because I am using Linux, I would type this:

java -cp runnable-texturepacker.jar com.badlogic.gdx.tools.texturepacker.TexturePacker 'Images/Sprites/DotsDots' '/home/netbook/AndroidStudioProjects/dotsdots/android/assets' dotsprite

After the texture packer runs, you will find two types of file in the output directory. There will be the .png files, which will be the actual sprite sheets, and there will be the .atlas file. The .atlas file is a text file that tells libGDX the location of each individual image on the sprite sheet.

If you want to just make a simple sprite sheet, feel free to stop reading here. I will discuss slightly little more technical stuff below, but it likely won't affect sprite or animation performance.

Note that the texturepacker defaults to sprite sheets that are 1024px x 1024px. In 2018 that seems fairly paltry, and if you want to change the sheet size, or any other options, you just need to include a file called 'pack.json' in the same directory as your individual image files. The pack.json file can have the following options:

{

pot: true,
paddingX: 2,
paddingY: 2,
bleed: true,
bleedIterations: 2,
edgePadding: true,
duplicatePadding: false,
rotation: false,
minWidth: 16,
minHeight: 16,
maxWidth: 1024,
maxHeight: 1024,
square: false,
stripWhitespaceX: false,
stripWhitespaceY: false,
alphaThreshold: 0,
filterMin: Nearest,
filterMag: Nearest,
wrapX: ClampToEdge,
wrapY: ClampToEdge,
format: RGBA8888,
alias: true,
outputFormat: png,
jpegQuality: 0.9,
ignoreBlankImages: true,
fast: false,
debug: false,
combineSubdirectories: false,
flattenPaths: false,
premultiplyAlpha: false,
useIndexes: true,
limitMemory: true,
grid: false,
 scale: [ 1 ],
scaleSuffix: [ "" ],
scaleResampling: [ bicubic ]

}

You only need to include the options you want to change, any unlisted options will just return to the default settings. As for the sprite sheet size, I believe that on most modern devices that sheets of 4096px x 4096px will work fine. However, old devices, particularly those using old versions of OpenGL, will not work. I thought about this personally, and decided that it was more important for me to maintain backwards compatibility than it is to minimize texture binding resource use, so I decided to stay with the default. Of course that decision is up to each developer, so you can decide on your own.

Next, if your images don't fit all onto one sprite sheet, it might be a good idea to separate them into subdirectories in your input directory. By doing this, you can ensure that the texturepacker saves related files on the same sprite sheets. Because the libGDX texturepacker tries to create the most efficient packing result, related sprites could be spread across several sprite sheets, which would be very inefficient.

Finally, if you want to run the texturepacker as part of your app, it can be done. We won't cover it here, because it seems to be of limited value to hobbyist developers that are likely working alone, but the libGDX wiki discusses it in detail.

*I have no idea what Dot's Dots is going to turn out to be, I am creating it on the fly as part of this tutorial. Shhhh!

Monday, May 14, 2018

Compiling Your libGDX Project

So now that you have imported a libGDX project into Android Studio, you will probably notice that it doesn't look like a normal Android Studio project, and also that it won't compile right away.

This tutorial will explain the changes needed to be made so that you can compile your libGDX project for Android, desktop and HTML.

Android:

So if you tried to compile your new project you probably got a Gradle error that looks something like this:


What happens here is that currently (as of early 2018) projects imported from libGDX use a Gradle version of something like 2.14.1 and libGDX needs a Gradle version of 3.5 or above. Conversely, you will also get an error if you are using any Gradle version above 4.0, which would be the default of Android Studio. In either case, we want to change the Gradle version. To do this, we open the 'gradle-wrapper.properties' file under the 'Gradle Scripts' heading in the Project window.

We want to change the distributionURL line to:
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip
If you followed my previous tutorial in setting up the libGDX project in Android Studio, this should be all you need to do in order to make the APK able to be compiled.

If you are still having trouble, here are some other problems I have faced.

Error:(2, 0) Plugin with id 'jetty' not found

This error is caused when you are using a Gradle version above 4.0. Follow the steps above to change the Gradle version to 3.5, but additionally follow these steps:

1. Open the project level 'build.gradle' file (again under the 'Gradle Scripts' heading in the Project window)
2. Change the 'classpath' version:
classpath 'com.android.tools.build:gradle:2.3.3'
3. In the same file, under 'repositories' remove:
google()
NOTE: There are two places to do #3, under 'buildscript' and under 'allprojects'

Error: Plugin with id 'gwt' not found

If you run into this error, there is a simple solution. Just go into the project level 'build.gradle' file (again under the 'Gradle Scripts' heading in the Project window) and add the following line under the 'dependencies' inside the 'buildscript' tag:
classpath 'de.richsource.gradle.plugins:gwt-gradle-plugin:0.6'

Desktop: 

The easiest way to compile to desktop is to create a runnable JAR file. To do this, open the Gradle window by clicking the vertical 'Gradle' button on the right side. In that menu, expand ':desktop', then expand 'other', and finally click on 'dist'.

Once it is finished compiling, you can find the JAR file in this directory:
[APPLICATION PATH]/desktop/build/libs

If you end up having an error, read below.

Running the desktop app from Android Studio

This is easy, once you know what to do. Go into the Project Window and expand the 'Desktop' folder. Several folders will open and at the end there will be a java file 'DesktopLauncher'. Right click that and select 'Run DesktopLauncher.main()'. Unfortunately, this will cause an error. It will say something like Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: Couldn't load file: badlogic.jpg. If this is the case, the answer is simple.

Go into the run menu at the top of the Android Studio window and click on 'Edit Configurations...' This will open the Run/Debug Configurations Menu. Click on 'DesktopLauncher' from the dropdown menu on the left. Now, on the right side you can see an item called 'Working directory:'. To fix this error just append 'android/assets' to the directory listed. It should look like:
[APPLICATION PATH]/android/assets

This should allow it to run, but if you have another error see below.

"Exception in thread 'main' java.lang.NoClassDefFoundError: com/badlogic/gdx/ApplicationListener"

To solve this problem, we need to run the app as a JAR application. To do this, go into the run menu at the top of the Android Studio window and click on 'Edit Configurations...' This will open the Run/Debug Configurations Menu. In this menu, first click the green plus sign in the top left. Select 'JAR Application' from the drop down menu. You will see something like this:
You need to enter the 'Path to JAR:' which will be:
[APPLICATION PATH]/desktop/build/libs/desktop-1.0.jar
Next, in the 'Before Launch' section at the bottom, hit the green plus and add 'Run Gradle Task'. The 'Gradle Project' should be found at:
[APPLICATION PATH]/desktop/build.gradle
Finally, in the 'Tasks' box, type:
desktop:dist

And that's it. You should be able to run your app on the desktop as a JAR application by clicking the 'Select Run/Debug Configuration' button in the menu and selecting 'JAR Application'. Then just click the green run arrow to the right and wait for it to compile.

HTML:

Let's be honest, libGDX doesn't play well with HTML. But, if you are like me, you want to be a completionist and force it to work. Well, be forewarned: you will have to deal with a lot of code workarounds in order to make your application compile to HTML. For the time being, though, let's just look at how to actually get it to compile.

In Android Studio, open the Gradle window by clicking the vertical 'Gradle' button on the right side. In that menu, expand ':html', then expand 'other', and finally click on 'dist'. That will build your web app.

To find the files, you need to go into:
[APPLICATION PATH]/html/build/dist
All of the files will be in that folder and its subfolders.

A word of warning, though. Even with the default libGDX app (showing a picture on a red background), it doesn't seem to work in every browser. For example, on my Linux machine, it works fine on Seamonkey (2.49.1) but not on Chrome (63.0.3239.132). At some point in the future I might come back to figure out why, but for the time being I am going to accept that it compiles and leave it at that.