The simplest 3D game on libGDX for Android in 200 lines of code
I teach at Samsung IT school Android programming for students. The training program covers a wide variety of topics. Among others, there is one lesson that introduces students to the basics of 3D graphics for Android. The standard teaching material for this lesson seemed very useless to me for several reasons:
Therefore, I decided to prepare my lesson that describes the basics of using libGDX for Android, and since I am preparing this material anyway, at the same time post it here - on the hub In this lesson we will make the simplest 3D game for Android, a screenshot of which you can see in the introduction to the article. So, interested, welcome to cat. Why libGDX? Firstly, the code must be in Java, because we teach students specifically Java programming. This narrows the choice. Secondly, libGDX was very easy to learn. In my conditions, this is a great advantage, outweighing other shortcomings.

The idea of the game is very simple: you are the co-pilot of a fighter in charge of weapons systems. You need to have time to shoot with laser weapons in time when the engine of the enemy ship is in the crosshairs of the sight, while your first pilot tries not to let himself off his tail. That is, in fact, the gameplay is described by the phrase "click on time."
In the course of this lesson, we will only need Android Studio 1.5 (the version may differ, here I gave the one with which I definitely succeeded).
First, we need to download the wizard for creating a project from libGDX, which greatly simplifies the task of initially setting up the project (you can download the link in the wiki instructions for the libGDX project ). That's what the settings I drove there:

We import the resulting project into Android Studio and begin working with the code itself. The main game code is in the file MyGdxGame.java (if you named this class the same way I did). Delete the template code and start writing your own:
Here we create a new camera with a viewing angle of 67 degrees (which is a fairly often used value) and set the aspect ratio of the width and height of the screen. Then we set the camera’s position to the point (150, -9, 0) and indicate that it will look at the center of coordinates (since that’s where we plan to place the pyramid around which the gameplay will be built). Finally, we call the update () utility method to apply all of our changes to the camera.
Now you can portray something that we will look at. Of course, we could use some kind of 3D model, but now, in order to simplify the lesson, we will draw only a simple pyramid:
Here we create an instance of ModelBuilder, which is designed to create models in code. Then we create a simple model of the cone with dimensions 20x120x20 and the number of faces 3 (which ultimately gives the pyramid) and set the material to green. When we create the model, at least Usage.Position must be specified. Usage.Normal adds normals to the model so that lighting can work correctly.
The model contains everything you need to draw and manage your own resources. However, it does not contain information exactly where to draw. So we need to create a ModelInstance. It contains location, rotation, and scale options for rendering the model. By default, it is drawn in (0, 0, 0) so we just create a ModelInstance, which is rendered in (0, 0, 0). But in addition, we also call the transform.setToRotation () method to rotate our pyramid 120 degrees along the Z axis (this is better seen from the camera’s position).
The model needs to be freed after use, so we add some code to our Dispose () method.
Now let's draw our model instance:
Here we add the Create ModelBatch to the create method, which is responsible for rendering and initializing the model. In the render method, we clear the screen, call modelBatch.begin (cam), draw our ModelInstance, and then call modelBatch.end () to complete the drawing process. Finally, we need to release modelBatch to make sure that all resources (for example, the shaders that it uses) are correctly released.

It looks pretty good, but a little lighting could improve the situation, so let's add it:
Here we add an instance of Environment. We create it and set the ambient (scattered) light (0.4, 0.4, 0.4) (note that the transparency value is ignored). Then we create a DirectionalLight (directional light) with color (0.8, 0.8, 0.8) and direction (10, 10, 20). I assume that you are already familiar with the light sources in general, although here everything is so pretty obvious. Finally, during rendering, we pass the created environment to the model handler.

Since we are still writing a game, it would not hurt to add a little bit of dynamics to a static picture. Let's make the camera move slightly with each drawing. It is appropriate to say here about the life cycle of a libGDX application. At start, the create () method is called, in which it is appropriate to place all initialization. Then, the render () method is called N times per second, where N is your FPS. This method draws the current frame. Therefore, to add dynamism to the application, we just need to somehow change the parameters of our game objects in render ().
Here we create the illusion that our pyramid is moving, although in fact the camera is moving, through which we look at it. At the beginning of the game, the create () method randomly selects the increment value Vpos [i] for each coordinate (speed). At each scene redraw in the render () method, the value of the change step is added to the coordinates. If we went beyond the established boundaries for changing coordinates, then we return the coordinates to these boundaries and generate new speeds so that the camera starts moving in the other direction. cam.position.set () actually sets the camera to new coordinates calculated according to the law described above, and cam.update () completes the process of changing camera parameters.
It can be noted that on different devices the speed of the pyramid will be different due to the difference in FPS and, accordingly, the number of render () calls per second. It’s good to add here the dependence of the increment of coordinates on time between frames, then the speeds would be the same everywhere. But we will not do this in order not to complicate the project.

Now let's make a game HUD:
Please note that the rotation and shift parameters (translate (x, y, z) method) of the pyramid are changed so that it is in the center of the screen and directed to the same place where our camera is looking. That is, at the start of the game, we are on the same course with the enemy and look him directly at the engines.
Here we create 2 text labels. The label label is used to display in-game information (FPS, game time, and hit statistics). The crosshair label is drawn in red and contains only one character - "+". This shows the player the middle of the screen - his scope. For each of them, in the new Label constructor (<TEXT>, new Label.LabelStyle (font, <COLOR>)), a style is specified that includes the font and color of the inscription. Labels are passed to the Stage object by the addActor () method, and, accordingly, are drawn automatically when the Stage is drawn.
In addition, for the crosshair label, the setPosition () method sets the position - the middle of the screen. Here we use the screen sizes (Gdx.graphics.getWidth (), ... getHeight ()) to calculate where our plus sign should be placed so that it appears in the middle. There is still a small dirty hack: setPosition () sets the coordinates of the lower left corner of the inscription. So that the center of the plus sign appears in the center of the screen, I subtract the constants 3 and 9 empirically (that is, at random) from the obtained value. Do not use this approach in full-fledged games. Just a plus sign in the middle of the screen is not serious. If you need crosshairs, you can use sprites .
Each time we draw, we create text through StringBuilder, where we put everything that we want to display at the bottom of the screen: FPS, time in the game, number of hits and rating. The setText () method allows you to set the label text, which we do in render () over and over again.

True, we can’t shoot yet. It's time to fix this flaw.
Note that the description of the MyGdxGame class has now changed. Here we inherit from the InputAdapter and implement the ApplicationListener interface. Such a structure will allow us to save our code unchanged, but supplement it with the ability to process user input. A line is added to the create () method registering our class as an input handler. We simply must implement the pause () and resume () methods, since the InputAdapter is abstract.
All math hit calculation is in render (). We check whether the camera’s coordinates are in that zone so that our opponent is in the center of the screen on the same course with us (whether the Y and Z coordinates are within the start ± zone). If we are on the same course, then you can shoot: set isUnder = true and make the scope a brighter red. Again, this simplicity of determining a hit is a trick based on thestupidity of simplicity, some convention of the gameplay. In general, libGDX has tools for determining which 3D models are in the touch area in the general case .
Touch processing methods are called touchDown (finger touched the screen) and touchUp (finger removed from the screen). These methods accept touch coordinates, but we will not use them here. In fact, it is enough for us to determine whether the camera is now in that position to look at the pyramid directly. If this is the case (the user clicked on time), then in touchDown we begin to count the time how much the laser fried the hostile pyramid. If not, then reduce the user's points by dividing in half (penalty for miss). When the user releases his finger, we check to see if he released it too late. If you let go late, then fine, if on time (the laser was still frying the target), then add points.
In general, the game is ready, but I want it to look more decent, and the pyramid is pretty boring. So, as an optional addition to the lesson, you can still implement a normal 3D model of an aircraft instead of a pyramid. Take this model and try to insert it into our game.
The model comes in 4 formats of different 3D editors. However, libGDX uses its binary model format, which you need to convert to use it in the game. For this, a special utility is provided - fbx-conv . Download the collected binaries and unpack it into some folder. There is a version for Windows, Linux and MacOS. The Windows version will start without unnecessary gestures, and for Linux and MacOS you need to run the command
Thus, we indicate to the utility where to look for its shared library libfbxsdk.so, which it requires to work. Run the utility:
Of course, you need to indicate your path to the model and use the binary for your OS. As a result, you get a file
Well, now we’ll plug this in the game:
Here we create an instance of the AssetManager class, which is responsible for loading game resources and instructing it to load our model. At each drawing, we check to see if the AssetManager has loaded the model yet (the update () method that returns boolean). If it loaded, then we shove our cute airplane into instance instead of the pyramid that got bored and set loading = false so that this inctance creation does not repeat on every frame, otherwise assets.update () will return true further during the whole time the application is running.
At startup, we get an exception

Bottom line: we wrote a very simple, but almost complete from the point of view of the means used (lighting, movement, HUD, touches, models) game, keeping within just 200 lines of code. Of course, there is much that can be improved: normal sight, skybox (sky or space around), the sounds of shots and flight, the game menu, the normal definition of a hit, etc. Nevertheless, the game already contains the very base of the game process and clearly shows The most important points in developing games on libGDX. I hope this lesson will contribute to the emergence of many new interesting games on Android from both my students and the audience of the Habr.
PS: here is the code on github and the apk-file of the game .
- Naked OpenGL is used, and since in practice in the programming of games ready-made engines are most often used, this is not very useful for students in the context of their own projects. Some might argue that seeing a pure OpenGL in a business is good for understanding the basics, but here comes the second flaw.
- The lesson is very incomprehensible. A typical student, albeit knowledgeable in programming, does not have a sufficient base to understand much of what is described in the lesson (for example, many matrices will be taught only at the university).
- At the end of the lesson we come to the result - drawing 3 triangles using OpenGL tools. It is so far from a real 3D game that it can easily discourage the student.
Therefore, I decided to prepare my lesson that describes the basics of using libGDX for Android, and since I am preparing this material anyway, at the same time post it here - on the hub In this lesson we will make the simplest 3D game for Android, a screenshot of which you can see in the introduction to the article. So, interested, welcome to cat. Why libGDX? Firstly, the code must be in Java, because we teach students specifically Java programming. This narrows the choice. Secondly, libGDX was very easy to learn. In my conditions, this is a great advantage, outweighing other shortcomings.

The idea of the game is very simple: you are the co-pilot of a fighter in charge of weapons systems. You need to have time to shoot with laser weapons in time when the engine of the enemy ship is in the crosshairs of the sight, while your first pilot tries not to let himself off his tail. That is, in fact, the gameplay is described by the phrase "click on time."
In the course of this lesson, we will only need Android Studio 1.5 (the version may differ, here I gave the one with which I definitely succeeded).
First, we need to download the wizard for creating a project from libGDX, which greatly simplifies the task of initially setting up the project (you can download the link in the wiki instructions for the libGDX project ). That's what the settings I drove there:

We import the resulting project into Android Studio and begin working with the code itself. The main game code is in the file MyGdxGame.java (if you named this class the same way I did). Delete the template code and start writing your own:
public class MyGdxGame extends ApplicationAdapter {
public PerspectiveCamera cam;
final float[] startPos = {150f, -9f, 0f};
@Override
public void create() {
cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
cam.position.set(startPos[0], startPos[1], startPos[2]);
cam.lookAt(0, 0, 0);
cam.near = 1f;
cam.far = 300f;
cam.update();
}
}
Here we create a new camera with a viewing angle of 67 degrees (which is a fairly often used value) and set the aspect ratio of the width and height of the screen. Then we set the camera’s position to the point (150, -9, 0) and indicate that it will look at the center of coordinates (since that’s where we plan to place the pyramid around which the gameplay will be built). Finally, we call the update () utility method to apply all of our changes to the camera.
Now you can portray something that we will look at. Of course, we could use some kind of 3D model, but now, in order to simplify the lesson, we will draw only a simple pyramid:
public class MyGdxGame extends ApplicationAdapter {
...
public Model model;
public ModelInstance instance;
@Override
public void create() {
...
ModelBuilder modelBuilder = new ModelBuilder();
model = modelBuilder.createCone(20f, 120f, 20f, 3,
new Material(ColorAttribute.createDiffuse(Color.GREEN)),
VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal);
instance = new ModelInstance(model);
instance.transform.setToRotation(Vector3.Z, 120);
}
@Override
public void dispose() {
model.dispose();
}
}
Here we create an instance of ModelBuilder, which is designed to create models in code. Then we create a simple model of the cone with dimensions 20x120x20 and the number of faces 3 (which ultimately gives the pyramid) and set the material to green. When we create the model, at least Usage.Position must be specified. Usage.Normal adds normals to the model so that lighting can work correctly.
The model contains everything you need to draw and manage your own resources. However, it does not contain information exactly where to draw. So we need to create a ModelInstance. It contains location, rotation, and scale options for rendering the model. By default, it is drawn in (0, 0, 0) so we just create a ModelInstance, which is rendered in (0, 0, 0). But in addition, we also call the transform.setToRotation () method to rotate our pyramid 120 degrees along the Z axis (this is better seen from the camera’s position).
The model needs to be freed after use, so we add some code to our Dispose () method.
Now let's draw our model instance:
public class MyGdxGame extends ApplicationAdapter {
...
public ModelBatch modelBatch;
@Override
public void create() {
modelBatch = new ModelBatch();
...
}
@Override
public void render() {
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
modelBatch.render(instance);
modelBatch.end();
}
@Override
public void dispose() {
model.dispose();
modelBatch.dispose();
}
}
Here we add the Create ModelBatch to the create method, which is responsible for rendering and initializing the model. In the render method, we clear the screen, call modelBatch.begin (cam), draw our ModelInstance, and then call modelBatch.end () to complete the drawing process. Finally, we need to release modelBatch to make sure that all resources (for example, the shaders that it uses) are correctly released.

It looks pretty good, but a little lighting could improve the situation, so let's add it:
public class MyGdxGame extends ApplicationAdapter {
...
public Environment environment;
@Override
public void create() {
environment = new Environment();
environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, 10f, 10f, 20f));
...
}
@Override
public void render() {
...
modelBatch.begin(cam);
modelBatch.render(instance, environment);
modelBatch.end();
}
}
Here we add an instance of Environment. We create it and set the ambient (scattered) light (0.4, 0.4, 0.4) (note that the transparency value is ignored). Then we create a DirectionalLight (directional light) with color (0.8, 0.8, 0.8) and direction (10, 10, 20). I assume that you are already familiar with the light sources in general, although here everything is so pretty obvious. Finally, during rendering, we pass the created environment to the model handler.

Since we are still writing a game, it would not hurt to add a little bit of dynamics to a static picture. Let's make the camera move slightly with each drawing. It is appropriate to say here about the life cycle of a libGDX application. At start, the create () method is called, in which it is appropriate to place all initialization. Then, the render () method is called N times per second, where N is your FPS. This method draws the current frame. Therefore, to add dynamism to the application, we just need to somehow change the parameters of our game objects in render ().
public class MyGdxGame extends ApplicationAdapter {
...
final float bound = 45f;
float[] pos = {startPos[0], startPos[1], startPos[2]};
float[] Vpos = new float[3];
final float speed = 2f;
private float getSpeed() {
return speed * Math.signum((float) Math.random() - 0.5f) * Math.max((float) Math.random(), 0.5f);
}
@Override
public void create () {
...
// initialize speed
for (int i = 0; i < 3; i++){
Vpos[i] = getSpeed();
}
}
@Override
public void render() {
...
for (int i = 0; i < 3; i++) {
pos[i] += Vpos[i];
if (pos[i] <= startPos[i] - bound) {
pos[i] = startPos[i] - bound;
Vpos[i] = getSpeed();
}
if (pos[i] >= startPos[i] + bound) {
pos[i] = startPos[i] + bound;
Vpos[i] = getSpeed();
}
}
cam.position.set(pos[0], pos[1], pos[2]);
cam.update();
modelBatch.begin(cam);
modelBatch.render(instance, environment);
modelBatch.end();
}
}
Here we create the illusion that our pyramid is moving, although in fact the camera is moving, through which we look at it. At the beginning of the game, the create () method randomly selects the increment value Vpos [i] for each coordinate (speed). At each scene redraw in the render () method, the value of the change step is added to the coordinates. If we went beyond the established boundaries for changing coordinates, then we return the coordinates to these boundaries and generate new speeds so that the camera starts moving in the other direction. cam.position.set () actually sets the camera to new coordinates calculated according to the law described above, and cam.update () completes the process of changing camera parameters.
It can be noted that on different devices the speed of the pyramid will be different due to the difference in FPS and, accordingly, the number of render () calls per second. It’s good to add here the dependence of the increment of coordinates on time between frames, then the speeds would be the same everywhere. But we will not do this in order not to complicate the project.

Now let's make a game HUD:
public class MyGdxGame extends ApplicationAdapter {
...
protected Label label;
protected Label crosshair;
protected BitmapFont font;
protected Stage stage;
protected long startTime;
protected long hits;
@Override
public void create() {
...
instance.transform.setToRotation(Vector3.Z, 90).translate(-5,0,0);
font = new BitmapFont();
label = new Label(" ", new Label.LabelStyle(font, Color.WHITE));
crosshair = new Label("+", new Label.LabelStyle(font, Color.RED));
crosshair.setPosition(Gdx.graphics.getWidth() / 2 - 3, Gdx.graphics.getHeight() / 2 - 9);
stage = new Stage();
stage.addActor(label);
stage.addActor(crosshair);
startTime = System.currentTimeMillis();
}
@Override
public void render() {
...
StringBuilder builder = new StringBuilder();
builder.append(" FPS: ").append(Gdx.graphics.getFramesPerSecond());
long time = System.currentTimeMillis() - startTime;
builder.append("| Game time: ").append(time);
builder.append("| Hits: ").append(hits);
builder.append("| Rating: ").append((float) hits/(float) time);
label.setText(builder);
stage.draw();
}
@Override
public void resize(int width, int height) {
stage.getViewport().update(width, height, true);
}
}
Please note that the rotation and shift parameters (translate (x, y, z) method) of the pyramid are changed so that it is in the center of the screen and directed to the same place where our camera is looking. That is, at the start of the game, we are on the same course with the enemy and look him directly at the engines.
Here we create 2 text labels. The label label is used to display in-game information (FPS, game time, and hit statistics). The crosshair label is drawn in red and contains only one character - "+". This shows the player the middle of the screen - his scope. For each of them, in the new Label constructor (<TEXT>, new Label.LabelStyle (font, <COLOR>)), a style is specified that includes the font and color of the inscription. Labels are passed to the Stage object by the addActor () method, and, accordingly, are drawn automatically when the Stage is drawn.
In addition, for the crosshair label, the setPosition () method sets the position - the middle of the screen. Here we use the screen sizes (Gdx.graphics.getWidth (), ... getHeight ()) to calculate where our plus sign should be placed so that it appears in the middle. There is still a small dirty hack: setPosition () sets the coordinates of the lower left corner of the inscription. So that the center of the plus sign appears in the center of the screen, I subtract the constants 3 and 9 empirically (that is, at random) from the obtained value. Do not use this approach in full-fledged games. Just a plus sign in the middle of the screen is not serious. If you need crosshairs, you can use sprites .
Each time we draw, we create text through StringBuilder, where we put everything that we want to display at the bottom of the screen: FPS, time in the game, number of hits and rating. The setText () method allows you to set the label text, which we do in render () over and over again.

True, we can’t shoot yet. It's time to fix this flaw.
public class MyGdxGame extends InputAdapter implements ApplicationListener {
...
final float zone = 12f;
boolean isUnder = false;
long underFire;
@Override
public void create() {
...
Gdx.input.setInputProcessor(new InputMultiplexer(this));
}
@Override
public void render() {
if (Math.abs(pos[1] - startPos[1]) < zone &&
Math.abs(pos[2] - startPos[2]) < zone) {
isUnder = true;
crosshair.setColor(Color.RED);
} else {
isUnder = false;
crosshair.setColor(Color.LIME);
underFire = 0;
}
...
}
@Override
public void pause() {}
@Override
public void resume() {}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
if (isUnder) {
underFire = System.currentTimeMillis();
} else {
hits /= 2;
}
return true;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
if (isUnder && underFire != 0) {
hits += System.currentTimeMillis() - underFire;
underFire = 0;
} else {
hits /= 2;
}
return false;
}
}
Note that the description of the MyGdxGame class has now changed. Here we inherit from the InputAdapter and implement the ApplicationListener interface. Such a structure will allow us to save our code unchanged, but supplement it with the ability to process user input. A line is added to the create () method registering our class as an input handler. We simply must implement the pause () and resume () methods, since the InputAdapter is abstract.
All math hit calculation is in render (). We check whether the camera’s coordinates are in that zone so that our opponent is in the center of the screen on the same course with us (whether the Y and Z coordinates are within the start ± zone). If we are on the same course, then you can shoot: set isUnder = true and make the scope a brighter red. Again, this simplicity of determining a hit is a trick based on the
Touch processing methods are called touchDown (finger touched the screen) and touchUp (finger removed from the screen). These methods accept touch coordinates, but we will not use them here. In fact, it is enough for us to determine whether the camera is now in that position to look at the pyramid directly. If this is the case (the user clicked on time), then in touchDown we begin to count the time how much the laser fried the hostile pyramid. If not, then reduce the user's points by dividing in half (penalty for miss). When the user releases his finger, we check to see if he released it too late. If you let go late, then fine, if on time (the laser was still frying the target), then add points.
Addition: model of a fighter instead of a pyramid
In general, the game is ready, but I want it to look more decent, and the pyramid is pretty boring. So, as an optional addition to the lesson, you can still implement a normal 3D model of an aircraft instead of a pyramid. Take this model and try to insert it into our game.
The model comes in 4 formats of different 3D editors. However, libGDX uses its binary model format, which you need to convert to use it in the game. For this, a special utility is provided - fbx-conv . Download the collected binaries and unpack it into some folder. There is a version for Windows, Linux and MacOS. The Windows version will start without unnecessary gestures, and for Linux and MacOS you need to run the command
export LD_LIBRARY_PATH=/folder/where/fbx-conv/extracted/
Thus, we indicate to the utility where to look for its shared library libfbxsdk.so, which it requires to work. Run the utility:
./fbx-conv-lin64 -f space_frigate_6/space_frigate_6.3DS
Of course, you need to indicate your path to the model and use the binary for your OS. As a result, you get a file
space_frigate_6.g3db
that you need to put in the project android/assets
folder (the folder with application resources for the Android platform).About the difficulties of converting models for libGDX for those who want to use other models
In general, the libGDX + fbx-conv bundle is very problematic. I tried about a dozen free spacecraft models from http://tf3dm.com/ and http://www.turbosquid.com/before I managed to find this one that worked. The difficulties are very different. Sometimes the model in the game is obtained without textures, sometimes it loads normally, but just does not appear, and sometimes (this is most often) when loading the model, the game drops out with OutOfMemoryError. Of course, I understand that this is a mobile platform. But games in the Play Market show much more complex graphics and they have enough memory for this. Even the model that I ultimately used posed problems. It wasn’t normally converted from obj, but it turned out from 3ds. In light of this, we can say that for the time being libGDX with model support is a bit tight. You can use this engine for simple games, if you carefully select models or make them yourself with an eye for compatibility with libGDX. Or use more advanced engines likejMonkeyEngine .
Well, now we’ll plug this in the game:
public class MyGdxGame extends InputAdapter implements ApplicationListener {
...
public AssetManager assets;
public boolean loading;
@Override
public void create() {
...
assets = new AssetManager();
assets.load("space_frigate_6.g3db", Model.class);
loading = true;
}
@Override
public void render() {
if (loading)
if (assets.update()) {
model = assets.get("space_frigate_6.g3db", Model.class);
instance = new ModelInstance(model);
loading = false;
} else {
return;
}
...
}
@Override
public void dispose() {
model.dispose();
modelBatch.dispose();
}
}
Here we create an instance of the AssetManager class, which is responsible for loading game resources and instructing it to load our model. At each drawing, we check to see if the AssetManager has loaded the model yet (the update () method that returns boolean). If it loaded, then we shove our cute airplane into instance instead of the pyramid that got bored and set loading = false so that this inctance creation does not repeat on every frame, otherwise assets.update () will return true further during the whole time the application is running.
At startup, we get an exception
java.io.FileNotFoundException: SPACE_FR.PNG
. So the model file does not include textures, they need to be pushed separately. We take from the 4 presented the texture we like, rename it to SPACE_FR.PNG
, put it in assets
run. As a result, we get what is in the opening picture. Well, for starters - a gif with the gameplay: 
Bottom line: we wrote a very simple, but almost complete from the point of view of the means used (lighting, movement, HUD, touches, models) game, keeping within just 200 lines of code. Of course, there is much that can be improved: normal sight, skybox (sky or space around), the sounds of shots and flight, the game menu, the normal definition of a hit, etc. Nevertheless, the game already contains the very base of the game process and clearly shows The most important points in developing games on libGDX. I hope this lesson will contribute to the emergence of many new interesting games on Android from both my students and the audience of the Habr.
Sources:
- https://libgdx.badlogicgames.com/nightlies/docs/api/overview-summary.html
- http://www.todroid.com/android-gdx-game-creation-part-i-setting-up-up-android-studio-for-creating-games/
- https://xoppa.github.io/blog/basic-3d-using-libgdx/
- http://stackoverflow.com/questions/19699801/dewitters-game-loop-in-libgdx
- http://stackoverflow.com/questions/21286055/run-libgdx-application-on-android-with-unlimited-fps
- https://xoppa.github.io/blog/interacting-with-3d-objects/
- https://xoppa.github.io/blog/loading-models-using-libgdx/
PS: here is the code on github and the apk-file of the game .