aboutsummaryrefslogtreecommitdiff
path: root/teaching/gameofcode/05-libgdx-game-of-codes-cameras-their-controllers-and-more-input.md
diff options
context:
space:
mode:
Diffstat (limited to 'teaching/gameofcode/05-libgdx-game-of-codes-cameras-their-controllers-and-more-input.md')
-rwxr-xr-xteaching/gameofcode/05-libgdx-game-of-codes-cameras-their-controllers-and-more-input.md455
1 files changed, 455 insertions, 0 deletions
diff --git a/teaching/gameofcode/05-libgdx-game-of-codes-cameras-their-controllers-and-more-input.md b/teaching/gameofcode/05-libgdx-game-of-codes-cameras-their-controllers-and-more-input.md
new file mode 100755
index 0000000..d52c107
--- /dev/null
+++ b/teaching/gameofcode/05-libgdx-game-of-codes-cameras-their-controllers-and-more-input.md
@@ -0,0 +1,455 @@
+Title: 05. (LibGDX) Game of Codes: Cameras, their controllers and more Input
+Date: 2013-12-20 02:21
+Category: Game of Codes
+Tags: Guides
+Slug: 05-libgdx-game-of-codes-cameras-their-controllers-and-more-input
+Status: published
+
+Welcome back to the Game of Codes, an introduction series to the LibGDX
+framework. In the last edition we learned how to use Texture Atli and
+sort our assets in a way that we don't get lost in them (and also import
+them all in one place, our ResourcePacker). In this issue I promised you
+something about cameras and I realised that this would be the perfect
+opportunity to add some more action into our game. Let's begin!
+
+The first thing we'll want to do is create a new class in our utility
+package and call it something along the lines of CameraController,
+CameraInputAdapter or CameraManager. Let the class extend the
+InputAdapter and already create a few variables in it. An
+OrthgraphicCamera called camera and 4 Vector3 objects called current,
+mouse, last and delta. We'll need these to handle the scrolling logic
+for our camera.
+
+``` {.lang:java .decode:true}
+public class CameraController extends InputAdapter {
+ final OrthographicCamera camera;
+
+ private final Vector3 current = new Vector3();
+ private final Vector3 mouse = new Vector3();
+ private final Vector3 delta = new Vector3();
+ private final Vector3 last = new Vector3(-1, -1, -1);
+
+}
+```
+
+Be sure to give last three "-1" values and the rest you can leave empty.
+That's that, now create a constructor that takes the camera and stores
+it into the camera variable in our controller.
+
+Add the highlighted code into your games create() method to set up the
+CameraController. **Notice how we replaced the InputAdapter as the Input
+Processor and are now using the camController!!!**
+
+``` {.lang:java .mark:3-4 .decode:true}
+ camera = new OrthographicCamera();
+ camera.setToOrtho(false, w, h);
+ camController = new CameraController(camera);
+ Gdx.input.setInputProcessor(camController);
+ camera.update();
+```
+
+And that's where we'll take a step back and look at cameras for a
+moment. I already mentioned "unprojecting" in a previous tutorial and
+the term "Orthographic" has been flying around quite a lot recently. So
+I actually wanted to take the time and explain a few things about
+cameras because they'll make the whole ordeal less complicated (and also
+give you the ability to come up with your own camera code).
+
+![zyGF1](http://www.spacekookie.de/wp-content/uploads/2013/12/zyGF1.gif)
+
+These are the two camera types, left perspective and right orthographic.
+The main difference is that the perspective camera actively uses the
+third dimension to create the effect of depth while orthographic cameras
+will project everything onto the screen in a straight line. The third
+dimension can be used to move things behind one another but there is no
+effect of depth. Two objects could be apart by 500 pixels in the z-axis
+and still have the same size on-screen.
+
+![unprojectcameras](http://www.spacekookie.de/wp-content/uploads/2013/12/unprojectcameras.png)
+
+Camera projection is a bit tricky but it boils down to using two
+different coordinate sets. When you draw something on the screen every
+point in your window has a pixel coordinate from an origin (either top
+left or top right corner). But when you're using a camera that camera
+can scroll around, it can zoom and move. Therefor every point in the
+camera space has a different coordinate than the pixel coordinates on
+the screen. In the example of the picture above it is visible that the
+point on the screen (in pink) has a different coordinate on the map than
+on the screen (because the map and screen origin aren't the same).
+
+When registering clicks, drags or any kind of interaction all those
+coordinates will be in the screen-coordinate system. But we don't want
+the game to just set place in one screen width/ height so we move around
+and by calling "unproject" with a Vector3 we can translate the screen
+coordinates (with help of the camera) into world coordinates. Those two
+can be the same if the origins are the same but they can also be vastly
+different when on the other side of the game world.
+
+Don't forget to remember this! Your camera space will always move around
+while your pixel space will stay the same.
+
+If you have questions about that, post them in the comments below. But I
+hope this should be clear now. Now we should get back to the matter at
+hand.
+
+Controlling the Camera {style="text-align: justify;"}
+----------------------
+
+We're gonna have a look at zooming the camera first and limiting zoom
+range so that the player can't zoom out to see the entire world or zoom
+too far in that one pixel of the ship takes up the entire screen. But
+first up, I want you to do one things: go into the games Main method
+(the one that starts the Desktop client) and change three things.
+
+``` {.lang:java .decode:true}
+ cfg.useGL20 = true;
+ cfg.width = 1280;
+ cfg.height = 720;
+```
+
+Using OpenGL2 isn't important for now but we should do it anyways. It
+will make things look smoother and work faster but not be compatible
+with older (Android) devices. The second thing is just upgrading the
+game to 720p resolution which in 2013 is probably better. Aaaaaand JUST
+like that we have the same resolution as the Xbox One :')
+
+``` {.lang:java .decode:true}
+ @Override
+ public boolean mouseMoved(int screenX, int screenY) {
+ mouse.set(screenX, screenY, 0);
+ return false;
+ }
+
+ @Override
+ public boolean scrolled(int amount) {
+ float newZoom = camera.zoom * (1 + (amount < 0 ? 0.1f : -0.1f));
+ changeZoom(newZoom, mouse.x, mouse.y);
+ return true;
+ }
+```
+
+Add these two methods to our camera controller. The mouseMoved method
+should be fairly trivial, we only log the current mouse location into
+our lovely vector, with the z-axis set to "0" and return with false.
+
+The second method is the scrolled(. . .) method that gets passed in an
+amount (-1 or 1, depending on the mouse-wheel direction) and executes
+some code to calculate the zoom.
+
+First up we create a new float called newZoom and take the current
+camera zoom and multiply it by 1 + either 0.1 or -0.1, depending on
+whether amount was 1 or -1. This is a general syntax that can be used, a
+shortened if-statement:
+
+``` {.lang:java .decode:true}
+(a < b ? 1 : 2)
+
+means:
+"Is a smaller than b? If that's the case return 1, if not, return 2"
+```
+
+We could have written a whole nestation of if-statements for it but why
+do that if we can just write one line.
+
+The next line calls "changeZoom" which is a method that we have yet to
+implement and gives it the newZoom value, as well as the latest
+mouse-pointer-coordinates. This means that the picture will zoom towards
+the mouse, like in any decent game. If you wanted it to zoom towards the
+center of the screen you could use this.
+
+``` {.lang:java .decode:true}
+changeZoom(newZoom, Gdx.graphics.getWidth() / 2, Gdx.graphics.getHeight() / 2);
+```
+
+Now let's have a look at what we need to do in the changeZoom(. . .)
+method we have yet to write.
+
+- We need to log the current screen position to translate the screen
+ by amount x and y to fit the new zoom position
+- We need to check if zoom should be applied (aka if inside the zoom
+ boundaries)
+- Apply the zoom, update the camera and check the new camera position
+- Then calculate how to move the camera to its new position
+
+So there will be a tiny tiny delay between zooming and panning to a side
+but in practise it's not noticeable. So let's get to work.
+
+First you should add another vector to the list, this time just a
+Vector2, that will hold our zoom boundaries for us. I just went with two
+hardcoded values that I found worked out quite well in practise. But
+you're free to experiment around.
+
+``` {.lang:java .decode:true}
+ /* x = min, y = max */
+ Vector2 zoomBounds = new Vector2(0.45f, 0.75f);
+```
+
+Next up, we can go create our method and tackle the first two bulletins
+on our list.
+
+``` {.lang:java .decode:true}
+ public void changeZoom(float zoom, float x, float y) {
+
+ Vector3 before = new Vector3(x, y, 0);
+ camera.unproject(before);
+
+ if (zoom <= zoomBounds.x || zoom >= zoomBounds.y) {
+ return;
+ }
+ }
+```
+
+Returning from a void method with no value will just jump out of the
+method. It's an easier why than to nest the entire algorithm into an if
+or the following else-statement. The statement just checks if the zoom
+value that we pass into the method.
+
+Now that we've shown that the zoom is valid we can apply it and update
+the camera. Then we can store the new location vector, unproject it and
+afterwards translate the camera from the old position to the new by
+subtracting the two vectors and thus getting the connection vector.
+
+``` {.lang:java .decode:true}
+ camera.zoom = zoom;
+ camera.update();
+ Vector3 after = new Vector3(x, y, 0);
+ camera.unproject(after);
+
+ camera.translate(before.x - after.x, before.y - after.y, 0);
+```
+
+Go ahead and try that for a bit. You'll see that the camera won't zoom
+past our barriers which means that the player can be focused on their
+ship and the environment at times and still zoom out a bit and get some
+sort of overview. It was important to having implemented this first
+because now we're gonna tackle the much harder subject of scrolling
+around (by clicking and dragging) and also setting boundaries around the
+map that limit the player from scrolling past our beautiful star map and
+into the nothingness that is the white background colour :O
+
+Start by adding two more methods: touchDragged, touchUp and update().
+
+``` {.lang:java .decode:true}
+ @Override
+ public boolean touchDragged(int x, int y, int pointer) {
+ camera.unproject(current.set(x, y, 0));
+ return false;
+ }
+
+ @Override
+ public boolean touchUp(int x, int y, int pointer, int button) {
+ return false;
+ }
+
+ public void update() {
+
+ }
+```
+
+And start by setting the current vector to the x and y values passed
+into the method as they'll be updating the current vector. Then
+unproject that bad girl to get the world coordinates of the actions.
+
+Next we want to check if we've already dragged. And that's why I wanted
+you to give the last vector the value (-1,-1,-1) because this now allows
+us to use this value as a flag. See, because we're only using 0 in the
+z-dimention we can assume that if it's set to -1 it's one of our flags.
+So go ahead and check if last is (-1,-1,-1) and if it is NOT! set the
+delta vector to the last coordinates and unproject that as well.
+
+``` {.lang:java .decode:true}
+if (!(last.x == -1 && last.y == -1 && last.z == -1)) {
+ camera.unproject(delta.set(last.x, last.y, 0));
+ }
+```
+
+Afterwards add two more things: we want delta to be the difference
+between the last point and our current point, so we need to subtract the
+two. And lastly we want to apply that manipulated delta vector to the
+camera so that it actually moves from our last position to the current
+position (on the delta vector). (Full if-statement to prevent confusion,
+I will add the full code again afterwards. This is really non-trivial).
+
+``` {.lang:java .decode:true}
+ if (!(last.x == -1 && last.y == -1 && last.z == -1)) {
+ camera.unproject(delta.set(last.x, last.y, 0));
+ delta.sub(current);
+ camera.position.add(delta.x, delta.y, 0);
+ }
+```
+
+After we're done with this we want the last vector to be set to our
+current coordinates because in the next call those need to be updated
+(as current will have been updated to our next position). Then we return
+with false because we want the event to trickle through every listener
+we have. What is that? I know...I'm getting ahead of myself, I will
+explain that in a bit!
+
+Now we can scroll around the camera but there is one thing we need to do
+and that is set last to (-1,-1,-1) again so that it can't activate the
+if-statement on the first call of touchDragged and gets assigned with
+the current coordinates.
+
+``` {.lang:java .decode:true}
+ @Override
+ public boolean touchUp(int x, int y, int pointer, int button) {
+ last.set(-1, -1, -1);
+ return false;
+ }
+```
+
+And here the full touchDragged(. . .) again as reference.
+
+``` {.lang:java .decode:true}
+ @Override
+ public boolean touchDragged(int x, int y, int pointer) {
+ camera.unproject(current.set(x, y, 0));
+
+ if (!(last.x == -1 && last.y == -1 && last.z == -1)) {
+ camera.unproject(delta.set(last.x, last.y, 0));
+ delta.sub(current);
+ camera.position.add(delta.x, delta.y, 0);
+ }
+ last.set(x, y, 0);
+ return false;
+ }
+```
+
+Now...if you try this out you'll see that we can zoom and scroll around
+on our lovely landscape. But the problem is that there are no bounds to
+keep us inside the landscape and the player can easily scroll too far
+and see the white background. Yikes.
+
+To fix this we need to go back to our main game class for a second and
+change the render() method a tiny bit.
+
+``` {.lang:java .mark:6-7 .decode:true}
+ @Override
+ public void render() {
+ Gdx.gl.glClearColor(1, 1, 1, 1);
+ Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
+
+ handler.update();
+ camController.update();
+
+ batch.setProjectionMatrix(camera.combined);
+ batch.begin();
+ space.draw(batch);
+ player.draw(batch);
+ batch.end();
+ }
+```
+
+What did I do here? I stopped updating the camera in the render method
+but instead started calling that update() method we created in the
+CameraController. We could of course have this code in the render method
+but it's good practise to not cluster that thing up too much. Go back to
+the camera controller and add that camera.update() call to the update()
+method again so that we don't break everything.
+
+And that's the point where I will once again bore you with mathematical
+theorems. (Actually it's not too bad). The subject at hand is clamping.
+For everybody familiar with this, go ahead and skip this paragraph. For
+everybody that is not, let me quote Wikipedia for you:
+
+*"In computer graphics, clamping is the process of limiting a position
+to an area. Unlike wrapping, clamping merely moves the point to the
+nearest available value."*
+
+So essentially we have a method called clamp(value, min, max); and the
+method tries to keep our value between the minimum and the maximum. In
+LibGDX this is done by calling the MathUtils class and asking for (the
+drunken) clamp.
+
+We will be clamping the cameras x and y position manually but we have
+another problem. See...we have our origin in the bottom left, but the
+camera position is measured at its center. And because we're zooming in
+and out the center doesn't always have the same position. So we need to
+adjust the boundaries for the camera dynamically. And we can do that by
+taking the size of the screen, multiplying it with the camera zoom and
+then dividing it by 2 to get the center.
+
+In code example.
+
+``` {.lang:java .decode:true}
+camera.position.x = MathUtils.clamp(camera.position.x, (Gdx.graphics.getWidth() * camera.zoom) / 2, max);
+```
+
+Now..for the maximum, we want the camera to stop on the other side of
+the texture, so we need to get the size of the texture from the
+ResPacker. But we also need to take the same offset we added to the
+minimum, because the center is still moved to its center. So instead of
+adding to "0" we now subtract from the size of the texture.
+
+``` {.lang:java .decode:true}
+ResPack.WORLD_BACKGROUND.getRegionWidth() - (Gdx.graphics.getWidth() * camera.zoom) / 2
+```
+
+And that's that. Here is the full code if you wanna check that you did
+it right. Of course we want to do this for both the X and the Y axis and
+we of course want to take the HEIGHT for the Y axis, not the width
+(looks at self in shame after not having found a bug with this).
+
+``` {.lang:java .decode:true}
+ public void update() {
+ camera.position.x = MathUtils.clamp(camera.position.x, (Gdx.graphics.getWidth() * camera.zoom) / 2,
+ ResPack.WORLD_BACKGROUND.getRegionWidth() - (Gdx.graphics.getWidth() * camera.zoom) / 2);
+ camera.position.y = MathUtils.clamp(camera.position.y, (Gdx.graphics.getHeight() * camera.zoom) / 2,
+ ResPack.WORLD_BACKGROUND.getRegionHeight() - (Gdx.graphics.getHeight() * camera.zoom) / 2);
+ camera.update();
+ }
+```
+
+is now our update method and it's glorious, isn't it? Try it out and
+marvel at our limitless glory and genius. And then realise that you can
+no longer move the ship. Well....remember? We kinda replaced the
+InputAdapter as our InputProcessor. But don't worry, there is a way
+around it! Remember when I spoiled you a few lines ago with trickle down
+something? Well..turns out that LibGDX offers you a way to add multiple
+InputProcessors with something called a *Multiplexer*.
+
+![Input
+Multiplexer](http://www.spacekookie.de/wp-content/uploads/2013/12/Screen-Shot-2014-01-10-at-23.57.26.png)
+
+Go into our main game class and add a new Object
+
+``` {.lang:java .decode:true}
+private InputMultiplexer plex;
+```
+
+And then in the create() method.
+
+``` {.lang:java .mark:6,12-14 .decode:true}
+ @Override
+ public void create() {
+
+ . . .
+
+ plex = new InputMultiplexer();
+
+ handler = new InputHandler(player);
+ camController = new CameraController(camera);
+
+ /* Input Controllers */
+ plex.addProcessor(camController);
+ plex.addProcessor(handler);
+ Gdx.input.setInputProcessor(plex);
+ }
+```
+
+Adding the CameraController first will give it a higher priority in the
+MultiPlexer. Meaning that it will first trigger the events in the
+CameraController and then trickle down to the other controllers. That's
+why I didn't want to return true in our drag-methods because that would
+stop the event and not pass it onto the next InputProcessor.
+
+And that's that, compile this and you'll see that you can fly the ship
+around and drag the camera over the map at the same time. And this is
+where I'll end this tutorial, I think I've loaded your brain with enough
+new stuff right now.
+
+Next time I want to take a look at actual gameplay. Letting the camera
+pan after the ship, letting the ship also have boundaries (same as the
+camera) and placing those other textures I made around in the world, as
+well as some well needed refactoring if we're gonna expand our feature
+list. But until next time, keep coding!