Hi there!
Indeed, the dev retired from indie-deving in 2019 and none of his projects will be updated.
Thanks for the fix to make the game run on 64-bit platforms! This game was developped when 32-bit was still predominant.
Thanks for all the comments guys !
@Radnap Well, sorry, but I didn't have time to talk about Box2DLights in my devlog ! The Box2DLights implementation was done only a few hours before the jam deadline, and after finishing my game, I just submitted it.... and slept ! haha
@CiderPunk and @mmachida Hahaha ! Yeah the game the difficulty of my game is definitely not balanced. I lacked time to test and balance the game. The level 6 is frustrating, but it's nothing compared to the level 7 hahaha ! Actually, I am stuck to this level. If you are pissed and want to try levels 7 and 8, just open the Cosmonaut.Data file with the notepad and change the level to "8" ! The Cosmonaut.Data file is located at C:\Users\username\.prefs.
Cosmonaut is a space adventure game.
After crossing the asteroid barrier, your spaceship is damaged.
No more oxygen!
No more artificial gravity!
In order to survive you must reach the survival capsule using your space suit and your jetpack.
Cosmonaut counts 24 levels.
For each level, the amount of oxygen and the amount of fuel are limited.
Use your jetpack wisely!
You can play the game here.
Finally ! I submitted my game !
After a huge rush:
Here is the last gameplay video :
And here is the link to play my game :
http://itch.io/jam/libgdxjam/rate/51123
Controls
Gameplay
This is obviously my last, short, devlog before I submit my game to the jam.
I wasted A LOT of time yesterday, trying to improve my graphics.I spent like 6 painful hours trying to find colors, redraw walls, and trying other things. But obviously, drawing is not for me ! hahaha
Thus, around 3 - 4 am, I decided it was time to stop everything, and start create levels, to at least have a game to submit. I created 2 short levels, that will be usefull for the player to grasp the gameplay and the concept. After these 2 levels, I went to bed, dead tired.
Here is a video showing these levels :
Now that I woke up, I'll create more levels. I guess if I reach 10 levels, that'll be enough for the jam.
Finally ! The last thing that missed in my game was sound !
Well... I mean, the last thing that missed to be submittable to the jam. Because if we talk about a release to the Play Store, for example, many things are missing, like... hum... where to start ? Quality graphics, polished gameplay, quality sounds, quality UI.... quality, quality, quality...
Let's get back to what interests us in this post : Sound !
In my game, Major Tom wanders in his wrecked spaceship. Without gravity, and more important, without air. Therefore, if I wanted realism, there wouldn't be any environmental sounds. There would be only Major Tom breath sound, and muffled sounds from contact between Major Tom and the environment.
But, I thought that this approach would be very interesting only if it's very well done, with quality recording of an actor playing stress breathing as the oxygen decreases... well... That was definitely not in my range.
Therefore, I added sounds.
I had to create sounds, the best I can, with Audacity... Generating a white noise, cutting the high frequencies and boosting the low frequencies to create a background sound. Recording with my phone (high quality sounds guys !) pressured air going through my lips, and putting the sound in backward... That's the kind of things I did yesterday. And for few sounds, I was desperate, and I took sounds on Freesound.org.
Implementing sounds in libGDX is very easy. There are 2 class : Sound and Music.
A Sound will be loaded in the memory, while a Music will streamed, so you can use large size high-quality music files without needing to use your memory.
First, I'll load my sound files in the AssetManager, in the LoadingScreen.java :
//Loading of the sounds game.assets.load("Sounds/Piston.ogg", Sound.class); game.assets.load("Sounds/Jetpack.ogg", Sound.class); game.assets.load("Sounds/Impact.ogg", Sound.class); game.assets.load("Sounds/Door.ogg", Sound.class); game.assets.load("Sounds/Fuel Refill.ogg", Sound.class); game.assets.load("Sounds/Oxygen Refill.ogg", Sound.class); game.assets.load("Sounds/Button On.ogg", Sound.class); game.assets.load("Sounds/Button Off.ogg", Sound.class); game.assets.load("Sounds/Exit.ogg", Sound.class); game.assets.load("Sounds/Gaz Leak.ogg", Sound.class); game.assets.load("Sounds/Background.wav", Music.class);
Then I can use these sounds in my game. You can see that I have 10 Sounds and 1 Music, for the background sound.
The background sound
The background sound is created in the GameScreen.java.
backgroundSound = game.assets.get("Sounds/Background.wav", Music.class); backgroundSound.setLooping(true); backgroundSound.play(); backgroundSound.setVolume(0.15f);
Note the setLooping() function, that allows you to loop the Music indefinitely.
The other sounds
I won't detail the use of every other sounds because, 1) it will be redundant and 2) I am REALLY lacking time to finish my entry before the deadline. I'll just show some basic things.
Each object (Hero, Door, Gas Leak, Switch...) will have its sound. Thus, to create the sound, in the creator I'll use :
sound = game.assets.get("Sounds/Gas Leak.ogg", Sound.class);
Then, there are many ways to play a sound. You can simply play the sound once with :
sound.play()
or, you can play sound in loop with :
sound.loop()
You can also set the volume, the pitch and balance the sound between the left and right speakers at the same time you play the sound with :
sound.play(volume, pitch, pan) or sound.loop(volume, pitch, pan)
And finally, you can give an ID to the sound at the same time you play it, which will be VERY useful to interact with a precise instance of a sound.
long soundId;
soundId = sound.play(volume, pitch, pan);
Then, when you want to stop every instance of a sound you can do
sound.stop();
But if you want to stop only a precise instance of a sound, you use its ID
sound.stop(soundId);
Ex : The gas leak sound :
In the Leak.java, I create the Sound in the creator() :
sound = game.assets.get("Sounds/Gas Leak.ogg", Sound.class); soundId = sound.loop(0.1f, MathUtils.random(0.98f, 1.02f), 0);
Note that when I create the sound, I set the pitch with a random float (MathUtils.random(0.98f, 1.02f)). This method is useful to have different object creating the same sound (in this case, the gas leak), without having an echo effect. Every gas leak will create a slightly different sound.
I want the sound to be louder as the hero get closer to the leak. For that, in the render (that is the active() function in the Leak.java) I have this line :
sound.setVolume(soundId, 4/(new Vector2(hero.heroBody.getPosition().sub(posX, posY)).len()));
With this code, I set the sound volume according to the distance between the hero and the leak, at every render step. The "4" is completely arbitrary. I chose it by try/error. This gave me satisfactory results. I have to fight a number that satisfies me for every object that has a distance dependent sound. For example, for the Piston.java, I use the number "10".
Well, that's pretty much it for the sounds. With slight differences for the different case, but you can see the codes of the differents objects, for the variations.
Well, that's cool this level selection screen, but what is even better is that we can use it ! You know, press a button a play the wanted level !
For that, in the LevelSelectionScreen.java, we need this code in the show() :
public void show() { Gdx.input.setInputProcessor(stage); for(int i = 0; i < levels.size; i++){ if(levels.get(i).getStyle() == textButtonStyle) buttonAction.levelListener(game, levels.get(i), (i+1)); } backButton.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y){ game.setScreen(new MainMenuScreen(game)); } }); }
You can see there is a for loop that calls a mysterious "buttonAction.levelListener(game, levels.get(i), (i+1));"
The ButtonAction.java is just an helper class to attribute the right action to the right button, it is to say, call the right level, when you press a button. Its levelListener() function is :
public void levelListener(final MyGdxGame game, TextButton bouton, final int niveau){ bouton.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y){ GameConstants.SELECTED_LEVEL = niveau; try{ game.setScreen(new GameScreen(game)); } catch(Exception e){ System.out.println("The level doesn't exist !"); } } }); }
And that's it ! You have a fully operational level selection screen !
Now, let's work on the sounds !
I started yesterday, and I can say it's extremely painful !
As the deadline to submit the game is approaching, I am lacking time to write devlogs. And my professional life doesn't help. I am also missing time to work on the game itself. Thus, I guess the devlogs will be shorter and less detailed for this last week.
Two days ago I worked onthe level selection screen. It looks like that :
You can find the code in the repository : LevelSelectionScreen.java.
Basically, the LevelSelectionScreen.java contains a Table, with many TextButtons. I already talked about the of Table and TextButton to create the HUD.
The key part of the LevelSelectionScreen.java is the positioning of the various TextButtons to form a nice table. Here is the code :
tableLevels = new Table(); tableLevels.defaults().width(Gdx.graphics.getWidth()/10).height(Gdx.graphics.getWidth()/10).space(Gdx.graphics.getWidth()/60); levels = new Array<TextButton>(); for(int i = 0; i < GameConstants.NUMBER_OF_LEVEL; i++){ TextButton textButton = new TextButton("" + (i + 1), textButtonStyle); levels.add(textButton); if((i + 1)%5 == 0) tableLevels.add(textButton).row(); else tableLevels.add(textButton); }
About this code :
I first sets the Table that will contains the various TextButtons. With the line
tableLevels.defaults().width(Gdx.graphics.getWidth()/10).height(Gdx.graphics.getWidth()/10).space(Gdx.graphics.getWidth()/60);
I set the height and width of the buttons, and the spacing between each button.
Then I create an array in which I will store the buttons. This array will be important to interact with the buttons.
And finally, with a for loop I create the TextButtons and arrange them in rows of 5 buttons. For that, I add each buttons to the Table, and every five buttons, I add the button to the Table and create a new row, with the lines :
if((i + 1)%5 == 0) tableLevels.add(textButton).row();
With this code, you obtain this level selection table :
Note in my code the GameConstants.NUMBER_OF_LEVEL, is a number I stored in the GameConstants.java. For now I set this number to 15, but when I see how much work I have to do before the end of the jam, I guess it will be 10 levels or less for the jam version of this game.
So now, I want to have a "back" button, to return to the main menu screen if I want to.
Thus I create the back button :
backButton = new TextButton("<", textButtonStyle); backButton.setWidth(Gdx.graphics.getWidth()/10); backButton.setHeight(Gdx.graphics.getWidth()/10);
And I want this to look good, I want the "back" button to be aligned with the table. For that, Actors have a very useful function, that is localToStageCoordinates. If I want the back button to be aligned to the left of the table, I need to know the coordinate of left side of a button in the first column. I'll get this coordinate with this line :
levels.get(0).localToStageCoordinates(new Vector2(0,0))
I take the 1st button, that I stored in the array levels, and I ask to translate Stage coordinate of its origin (new Vector2(0,0)) to the world system.
I also want my "back" button to be at the bottom of the table, and I want it to use the same spacing as the spacing between the buttons of the table. Thus, I set the position of the "back" button with this code :
backButton.setX(levels.get(0).localToStageCoordinates(new Vector2(0,0)).x); backButton.setY(levels.get(levels.size - 1).localToStageCoordinates(new Vector2(0,0)).y - backButton.getHeight() - Gdx.graphics.getWidth()/60);
And you obtain this screen :
hu... wait... what's this bullshit ?
The back button is in the middle of the table.
OK, here is the important part. If you want to translate the coordinate a button in the table to the world coordinate, you first need to add the table to the stage, draw it, and the get the coordinate.
In pseudo-code, you need to do that in the creator():
//Create buttons and put them in the tableLevels <span class="redactor-invisible-space">stage.addActor(tableLevels); stage.draw(); <span class="redactor-invisible-space"> <span class="redactor-invisible-space">//Create the "back" button and set its position stage.addActor(backButton);<span class="redactor-invisible-space"></span></span></span></span>
And now you've got this nice screen :
Now we want the player to be able to see which levels he has already done.
There are many ways to deal with that.
The bad solution
We could only create the buttons corresponding to the levels completed and the first next level to complete : If the player already completed levels 1 and 3, we could create a table containing the buttons 1, 2 and 3. Thus the player will know that the last button displayed correspond to the level he has to play. This solution will create inconsistency in the displaying of the table :
The good solutions
There are several way to make this look well. You could create a 2 TextButtonStyles, one for the locked levels, and one for the unlocked levels. This way you could display the buttons with different colors (for example) according on the locked/unlocked state of the level.
The solution I chose is to completely hide the button corresponding to the locked levels with this code, at the end of the creator() :
for(int i = 0; i < levels.size; i++){ if((i + 1) > Data.getLevel()){ levels.get(i).setTouchable(Touchable.disabled); levels.get(i).setVisible(false); } }
I check if the number of the button is greater than the unlock level number that I saved in the Preferences file "Data". I yes, I set the button invisible and untouchable.
And here is the result :
Then you only have to create a Label to display the "Chose a level" title to obtain the screen displayed in the gif, at the beginning of this post.
Before working on the sounds, I still have few things to do, in order to have a game fully playable :
Storing and loading small data, like a level number, is very easy in libGDX with the Preferences.
Firs, let's create the Data.java :
public class Data { public static Preferences prefs; public static void Load(){ prefs = Gdx.app.getPreferences("Data"); if (!prefs.contains("Level")) { prefs.putInteger("Level", 1); } } public static void setLevel(int val) { prefs.putInteger("Level", val); prefs.flush(); //Mandatory to save the data } public static int getLevel() { return prefs.getInteger("Level"); } }
In the Load() function, we check if the field "Level" exits. If not, we create it and attribute it a default value. Then, in the game we can access to this value with the getLevel() function and we can modify its value with the setLevel() function.
Loading the data
Before accessing the data "Level", we need to load the Preference file. We'll do that in the main activity, MyGdxGame.java, simply by using this line in the create() :
Data.Load();
Accessing the data
Then, when we need to know which level we unlocked, we can access to this number with the line :
Data.getLevel();
Saving data
At the end of a level, when we want to increment the number of the level we unlocked we only need to do :
Data.setLevel(Data.getLevel()++);
During the past 3 days, I drew. And the good news, for me, is that I think I'm done with the drawings for the libGDX Jam. I have everything I need for my game. I could do more, but, I don't have time, the jam ends in 8 days !
But well, I have the minimum to create levels : a few tiles for the background, and sprites for every objects of my game. At last !
Basically, for the code, I have two draw methods, in the Obstacle.java, and I call the needed method depending on the need to use a NinePatch or a TextureRegion.
Here are the draw() methods :
public void draw(SpriteBatch batch, TextureAtlas textureAtlas){ batch.setColor(1, 1, 1, 1); batch.draw(textureAtlas.findRegion(stringTextureRegion), this.body.getPosition().x - width, this.body.getPosition().y - height, width, height, 2 * width, 2 * height, 1, 1, body.getAngle()*MathUtils.radiansToDegrees); } public void draw(SpriteBatch batch){ batch.setColor(1, 1, 1, 1); ninePatch.draw(batch, this.body.getPosition().x - width, this.body.getPosition().y - height, 2 * width, 2 * height); }
So... what did I drew during these days ?
This sprites are used with the ObstacleLight.java. For now I have to sprites : one for boxes with square proportion and one for rectangles. These are wooden box sprites... doesn't really suit the space theme, I know. If I have time during next week end, I'll draw some metal boxes.
These sprites are used with the Leak.java.
For the gas leak, I had to create an animation, so I drew 10 sprites, and the Leak.java has its own draw() method :
public void draw(SpriteBatch batch, float animTime){ batch.setColor(1, 1, 1, 1); batch.draw(leakAnimation.getKeyFrame(animTime), this.body.getPosition().x - width, this.body.getPosition().y - height, width, height, 2 * width, 2 * height, leakScale, 1/leakScale, leakAngle); }
These sprites are used with the ObstacleDoor.java and the ItemSwitch.java.
The ItemSwitch.java has its own draw() method to be able to draw the right sprite depending on the switch being in the "on" or "off" state :
public void draw(SpriteBatch batch, TextureAtlas textureAtlas){ batch.setColor(1, 1, 1, 1); if(isOn){ batch.draw(textureAtlas.findRegion("SwitchOn"), this.swtichBody.getPosition().x - width, this.swtichBody.getPosition().y - height, 2 * width, 2 * height); } else{ batch.draw(textureAtlas.findRegion("SwitchOff"), this.swtichBody.getPosition().x - width, this.swtichBody.getPosition().y - height, 2 * width, 2 * height); } }
This sprite is used by the ObstacleMoving.java.
These sprites are use by the FuelRefill.java and OxygenRefill.java.
This sprites are used by the Exit.java.
This one took me A LOT of time. I wanted to create an animation for the level exit door. I created the animation with Spriter Pro.
Just for fun, here is a speed up video showing the process of creating the animation with Spriter Pro :
Drawing : Done !
Next : Sounds
OMG
I was a bit tired of only working on the drawings, thus I worked a feature that could be useful to grasp the levels structure : camera zoom-in/zoom-out.
I one of the first posts of this devlog, I detailed my camera class, MyCamera.java, that follows the hero and don't go outside of the level limits.
I thought that a zoom feature was missing :
Thus, I added these lines to the displacement() method of the MyCamera.java :
//Zoom-in/Zoom-out if (Gdx.input.isKeyPressed(Input.Keys.O)) { viewportWidth *= 1.01f; viewportHeight *= 1.01f; zoomLimit(); } else if (Gdx.input.isKeyPressed(Input.Keys.P)) { viewportWidth *= 0.99f; viewportHeight *= 0.99f; zoomLimit(); }
You can zoom out by pressing the "O" key, and zoom in by pressing the "P" key.
Why not "+" and "-" keys ?
There is a problem of mapping with the "+" and "-" of the numerical keypad. The code line
Input.Keys.PLUS
receives the input from the "+" of the numerical keypad , which is good. BUT, the code line
Input.Keys.MINUS
receives the input from the "-" above the "P" key, which is not good.
Therefore, I preferred to use 2 keys that are close from each other, "O" and "P".
Why do I modify viewportWidth and viewportHeight instead of uzing the zoom field of the camera ?
Because I am lazy. If I used the zoom field of the camera, I would have to take into account the zoom value in the code that I created at the beginning of the jam, and I didn't want waste time on that.
What the hell is that zoomLimit() method ?
Ho yeah, I almost forgot to talk about that method ! zoomlimit() is a method that... hu... limits the zoom. You can't zoom out of the level limits, and you can't zoom to closely, that wouldn't be playable.
And here is this zoomlimit() method :
public void zoomLimit(){ if(viewportWidth > GameConstants.LEVEL_PIXEL_WIDTH){ viewportWidth = GameConstants.LEVEL_PIXEL_WIDTH; viewportHeight = viewportWidth * GameConstants.SCREEN_RATIO; } else if(viewportWidth < GameConstants.SCREEN_WIDTH/2){ viewportWidth = GameConstants.SCREEN_WIDTH/2; viewportHeight = viewportWidth * GameConstants.SCREEN_RATIO; } else if(viewportHeight > GameConstants.LEVEL_PIXEL_HEIGHT){ viewportHeight = GameConstants.LEVEL_PIXEL_HEIGHT; viewportWidth = viewportHeight / GameConstants.SCREEN_RATIO; } else if(viewportHeight < GameConstants.SCREEN_HEIGHT/2){ viewportHeight = GameConstants.SCREEN_HEIGHT/2; viewportWidth = viewportHeight / GameConstants.SCREEN_RATIO; } }
And that's it ! Now you can zoom in/out at your convenience !
To draw everything that is not drawn by the TiledMapRenderer, I need to implement a draw() method in the entities I want to draw.
Basically, the way I built my code, I have the Obstacle entities, that represents almost everything the hero will interact or collide with (walls and various moving objects). Thus I need to create draw methods for every type of Obstacle. And that's where my code starts to be REALLY messy and dirty. No time for code optimization and code cleaning at this point of the jam, remember ?
So, there are some Obstacle entities that will require a NinePatch (Walls and Pistons), and some Obstacle entities that will require a Texture (all the other Obstacles I think). Texture and NinePatch don't use the same draw method. Then, I have to take this point into account.
Before seeing the code for the draw methods, let's see the simple part :
Before calling the different draw methods in the render of the GameScreen, I organize the different entities to be drawn in Arrays :
In the TiledMapReader.java, I store the Obstacles that need a Texture in an array called "obstacles" and I store the Obstacles that need a NinePatch in an array called obstaclesWithNinePatch .
For that, I only need to do
obstacles.add(obstacle);
or
obstaclesWithNinePatch.add(obstacle);
Once the various Obstacles stored in the right Array, I can draw them in the GameScreen.java, by calling the right draw method, whether the Obstacle uses a Texture or a NinePatch.
For that, I need to add this code in the render() of the GameScreen.java :
game.batch.begin(); for(Obstacle obstacle : mapReader.obstaclesWithNinePatch) obstacle.draw(game.batch); for(Obstacle obstacle : mapReader.obstacles) obstacle.draw(game.batch, textureAtlas); game.batch.end();
Then let's go the really messy part
The Obstacle.java was reorganize to have a constructor that need a TextureAtlas as argument, which is needed for the use of NinePatch.
Now Obstacle.java have these constructors :
public Obstacle(World world, OrthographicCamera camera, MapObject rectangleObject){ } public Obstacle(World world, OrthographicCamera camera, MapObject rectangleObject, TextureAtlas textureAtlas){ } public Obstacle(World world, OrthographicCamera camera, PolylineMapObject polylineObject){ setInitialState(polylineObject); }
And there are also two draw() methods.
1. Draw method that uses a Texture :
public void draw(SpriteBatch batch, TextureAtlas textureAtlas){ batch.setColor(0, 0, 0.1f, 1); batch.draw(textureAtlas.findRegion("WhiteSquare"), this.body.getPosition().x - width, this.body.getPosition().y - height, width, height, 2 * width, 2 * height, 1, 1, body.getAngle()*MathUtils.radiansToDegrees); }
2. Draw method that uses a NinePatch :
public void draw(SpriteBatch batch){ batch.setColor(1, 1, 1, 1); ninePatch.draw(batch, this.body.getPosition().x - width, this.body.getPosition().y - height, 2 * width, 2 * height); }
In the past, in the TiledMapReader, I had a for loop that checked what type of Obstacle need to be created, and by default, it created an Obstacle. I did a little change, and created a Wall class, and now, the for loop creates a Wall by default.
Here is the code of Wall.java :
public class Wall extends Obstacle{ public Wall(World world, OrthographicCamera camera, MapObject rectangleObject, TextureAtlas textureAtlas) { super(world, camera, rectangleObject, textureAtlas); create(world, camera, rectangleObject); ninePatch = new NinePatch(textureAtlas.findRegion("Wall"), 49, 49, 49, 49); ninePatch.scale(0.5f*GameConstants.MPP, 0.5f*GameConstants.MPP); } }
As you can see the Wall.java, we create a NinePatch with this line
ninePatch = new NinePatch(textureAtlas.findRegion("Wall"), 49, 49, 49, 49);
In Photoshop I drew an image of 100px by 100px dimension, and I packed it in the TextureAtlas with all the other images.Then, when I create the NinePatch with the above code line, I tell which texture will be the NinePatch, and I define which regions will stretch with the "49, 49, 49, 49" of the code which correspond to the coordinates of 4 lines that will split the chosen texture in 9 parts.
Then I resize the NinePatch so it's not HUGE, and it can incorporate well in my game with this line
ninePatch.scale(0.5f*GameConstants.MPP, 0.5f*GameConstants.MPP);
The case of the ObstaclePiston
Drawing the ObstaclePiston was a bit tricky, as it needs two NinePatch : one for the head and one for the axis that need to be positionned precisely.
I rewrote most of the ObstaclePiston.java, and I could spend a whole post on it. Just click the link to see the code in the repository.
After setting up the scrolling star background, I worked on the map itself. For that I use the amazing level editor Tiled.
First, I need to draw a tileset... I spent 3 evenings on this and I am not near finishing it. I have the minimum to create some levels... without variety and without beauty...
I made the (probably bad) decision to work with square tiles of 100 pixels x 100 pixels. From what I usually see, people use lower resolutions like 32px x 32px for example.
So why this 100 x 100 resolution ?
Well... the truth is I ABSOLUTELY suck at drawing. And it seems much easier to draw with higher resolutions. I wasted more than one evening trying to draw 32x32 tiles, but seriously... when we talk about "pixel art", the word "art" is not excessive. With low resolution, every single pixel you draw is important.
For example, just doing a gradient is an art in low resolution, there are some precise rules to make a dithering look good while with higher resolutions you can just go with the "aaaaaah fuck it, Photoshop as the right tool for that" way.
Then, once the 100x100 resolution chosen, I can draw the tileset and create a first level in Tiled. For now, I came up with this obviously unfinished tileset. As you can see, I am still completely in the process of creating it. I try to figure out many things like the size or the color of different elements.
I put the tileset in the folder android ---> assets ---> Levels and I also save the .tmx file generated by Tiled in the same folder.
After drawing the map, I can render it with few code lines in the GameScreen.java.
First we need to create the TiledMap and the TiledMapRenderer in the constructor :
tiledMap = new TmxMapLoader().load("Levels/Level 3.tmx"); tiledMapRenderer = new OrthogonalTiledMapRendererWithSprites(tiledMap, GameConstants.MPP, game.batch);
Then in the renderer() we need these lines :
tiledMapRenderer.setView(camera); tiledMapRenderer.render();
Very simple ! Here is the result :
Off course, the order you draw things is very important. If you want the game map to appear in front of the scrolling background I set up in the previous devlog, you must first draw the background, then draw the map, like this :
tiledMapRenderer.setView(camera); //Background game.batch.begin(); game.batch.draw(backgroundTexture, 0, 0, levelPixelWidth, levelPixelHeight, (int)(backgroundTime * 8), 0, (int)(levelPixelWidth * 20), (int)(levelPixelHeight * 20), false, false); game.batch.end(); //Game map tiledMapRenderer.render();
Where are the walls the Obstacles ??
As you can see in the above gif, there are still a lot of things rendered by the Box2d renderer, namely the walls and the objects.
To obtain this effect, I use a 100px x 100px image of stars that I repeat all over the level surface, and I scroll it slowly. The problem with that code is that I draw the background also outside the camera viewport. That's definitely a waste of GPU time, but, as I said, I definitely don't have time to look for a better option.
All the code is added to the GameScreen.java.
In the creator of the GameScreen.java you need to declare a Texture and make it repeatable :
backgroundTexture = new Texture(Gdx.files.internal("Images/Stars.jpg"), true); backgroundTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat);
Still in the creator, I calculate the screen dimensions :
levelPixelWidth = Float.parseFloat(tiledMap.getProperties().get("width").toString()) * GameConstants.PPT * GameConstants.MPP; levelPixelHeight = Float.parseFloat(tiledMap.getProperties().get("height").toString()) * GameConstants.PPT * GameConstants.MPP;
Then in the render() of the GameScreen.java I draw the Texture with the right SpriteBatch method:
game.batch.begin(); game.batch.draw(backgroundTexture, //Texture to load 0, //X position of the Texture 0, //Y position of the Texture levelPixelWidth, //Width of the Texture levelPixelHeight, //Height of the Texture (int)(backgroundTime * 8), //X offset of the texture 0, //Y offset of the texture (int)(levelPixelWidth * 20), //I have no fucking idea how to rationnalize these 2 factors but (int)(levelPixelHeight * 20), //the higher value, the smaller the size of a single Background Texture false, //Flip horizontal false); //Flip vertical game.batch.end();
You noticed in this method that in the X offset I entered (int)(backgroundTime * 8). backgroundTime is simply a float that I update in the render with this line :
backgroundTime += Gdx.graphics.getDeltaTime();
And that's it ! I now have stars scrolling in background !
Just in case you are interested :
I talked about the bad optimization of my code. With the previous code I draw the background even outside the viewport. Actually, I worked a lot on this issue, I even think too much for such a detail, at such a time of the Jam. I came up with that code, that draws the background with exactly the viewport size. The background also follows the camera position :
Vector3 posBackground = new Vector3(0,0,0); camera.project(posBackground); game.batch.begin(); game.batch.draw(backgroundTexture, -posBackground.x * camera.viewportWidth / Gdx.graphics.getWidth(), //Follows the camera -posBackground.y * camera.viewportHeight / Gdx.graphics.getHeight(), //Follows the camera camera.viewportWidth, //Camera viewport width camera.viewportHeight, //Camera viewport height (int)(backgroundTime * 8), 0, (int)(levelPixelWidth * 10), (int)(levelPixelHeight * 10), false, false); game.batch.end();
Here is the result
This is okay for the optimization, but not for the rendering. Once I draw the tiles over this background, it's very weird as the tiles "slide" over the background, when we look through windows, we have the feeling that the stars scroll very fast. To compensate, I have to take the camera speed into account in the Offset argument of the draw method, but I don't know how yet.
Just a quick devlog to tell I am still alive and working on my game. But I returned to work on monday, so I have much less time for the libGDX Jam, and I spent the 3 last evening drawings. And it was REALLY painful !
Here is a short video showing the latest graphical updates :
I'll write a more complete devlog tomorrow ! Now it's time to go to bed !
In the prévious devlog, I created a spritesheet containing all the images I need to create an Idle and a Fly Animation. This spritesheet was created with the libGDX Texture Packer, and comes with a .pack file, that contains the coordinates of every sprite in the spritesheet.
Thus I have the files Tom_Animation.png and Tom_Animation.pack. I put them in android ---> assets ---> Images
In the LoadingScreen.java, I load the spritesheet, like I load any Texture Atlas, with this single code line :
game.assets.load("Images/Tom_Animation.pack", TextureAtlas.class);
In the Hero.java, to create the animations, we need to add this lines in the constructor :
TextureAtlas tomAtlas = game.assets.get("Images/Tom_Animation.pack", TextureAtlas.class); Animation tomIdle = new Animation(0.1f, tomAtlas.findRegions("Tom_Idle"), Animation.PlayMode.LOOP); Animation tomFly = new Animation(0.1f, tomAtlas.findRegions("Tom_Fly"), Animation.PlayMode.NORMAL);
And that's it ! We created 2 Animations, based on 1 TextureAtlas. Easy, ain't it ?
Details of an Animation declaration :
To use the animations, I create a draw() function in the Hero.java. That draw() function will be called in the GameScreen.java, between a batch.begin() and a batch.draw().
Here is the draw() function of the Hero.java :
public void draw(SpriteBatch batch, float animTime){ if(Gdx.input.isKeyPressed(Keys.W) && fuelLevel > 0){ if(!fly){ GameConstants.ANIM_TIME = 0; fly = true; } batch.draw(tomFly.getKeyFrame(animTime), heroBody.getPosition().x - bodyWidth, heroBody.getPosition().y + bodyHeight - spriteHeight, bodyWidth, spriteHeight - bodyHeight, spriteWidth, spriteHeight, 1, 1, heroBody.getAngle()*MathUtils.radiansToDegrees); } else{ if(fly){ GameConstants.ANIM_TIME = 0; fly = false; } batch.draw(tomIdle.getKeyFrame(animTime, true), heroBody.getPosition().x - bodyWidth, heroBody.getPosition().y + bodyHeight - spriteHeight, bodyWidth, spriteHeight - bodyHeight, spriteWidth, spriteHeight, 1, 1, heroBody.getAngle()*MathUtils.radiansToDegrees); } }
About this code :
You can see in my code, that for there are parameter called bodyWidth, bodyHeight, and spriteWidth, spriteHeight.
Why ?
I could have drawn the sprites at exactly the same size as the Box2D body, but I would have a weird collision detection :
As you can see on the picture above, as my character is moving, in some frames, his limbs cover a smaller portion of the Box2D body, but, the body would still detect collisions in the non covered area... So I made the choice to create a body that has the same height as the sprites, but is thiner. Therefore, there will be som frames (mainly in the idle animation) with part of the limbs going out of the detection zone, which is OK, as limbs are flexible, it's not really a problem if we don't take into account the collision with small parts of them.
As the frame is not the same size as the Box2D body, in the draw function, I had to do a bit of mathematics to calculate the X and Y positions of the frames rotation center, as they don't correspond anymore to the Box2D body rotation center.
And here is the result !
I worked a bit more on Major Tom's design. I added a bit of light and shadows, to make it look less flat, and I added small details to the helmet. Oh ! and also, color to the logo on the shoulder ! Now the hero is 99% black and white, not 100%.
Here is a before/after pictures :
I am pretty sure I'll stick to it. Drawing is definitely a pain for me. I think I gave my best shot on this.
So now I am happy with my hero, what do I do with him ?
I chop him into parts ! Just like Dexter would do !
Oh noooooo ! Major Tom has been chopped !
Why did I do that ???
To create an animation, basically, there are 2 methods :
When you are like me, and you are not skilled in drawing, the second method is definitely the best. You'll save A LOT of time and frustration.
Therefore, I put all these body parts in the amazing software Spriter Pro.With this tool, you just drag and drops the body parts of your character in the main window. Once you placed them, you can draw "Bones", basically one bone for each part of the body you want to move, and you assign a body part to every bone. All that remains to do is move the limbs of your puppet, and the Spriter will interpolate the whole motion, it very intuitive and ridiculously easy. Just look the first of them tutorial videos, in 7 minutes you'll be able to do very cool animations !
The screen looks like that :
Then all you need to do is export your animation. You can export it in gif, or you can export the .png files. That's what we'll do, as libGDX needs a spritesheet with all the animations steps to create the in-game animation.
Here is the idle animation of Major Tom, floating in space. In-game, the animation will be slower.
I also created an animation that will be called Fly Animation, that will be displayed when Major Tom activates his jet pack.
Finally, I exported 11 png for the Fly Animation and 20 png for the Idle Animation, and packed them with the libGDX Texture Packer, to obtain this animation spritesheet :
The names of the different png are VERY important. All the png of a single animation must have the same name, followed by a number. For example, for the Idle animation I have Tom_Idle_000, Tom_Idle_001, Tom_Idle_002, ..., Tom_Idle_019. And in the libGDX code, to create the animation, I'll only refer to the name "Tom_Idle", and libGDX will be able to get all the pictures and put them in order.
We'll see the code in the next devlog !
And voilà ! I reach to the much dreaded part : Draw
Basically, all that remains is to give a visual identity to the game. Oh and there are also sounds... damn.
Starting from today, I completely come out of my area of "expertise " (If only I was an expert in coding... haha). The 17 remaining days will be really difficult, and I'll learn to draw...
Before doing the tileset, I started by the hero, Major Tom. And it was... painful. I spent 110 minutes on Photoshop, drawing shapes. Looking at examples on Google Image.
Well, I am somewhat satisfied with the result :
Here is a speed up videos showing the whole process in 2 minutes :
Notice that I drew Major Tom in high resolution ! I found it was easier. I'll have to scale it down. I hope it will render well once scaled down... Fingers crossed.
Now I have to animate Major Tom.
That will be painful again.
Here are the functions of the HUD.java :
public void draw(){ //Oxygen level game.batch.setColor(0,0,1,1); game.assets.get("fontHUD.ttf", BitmapFont.class).draw( game.batch, "Oxygen", posXOxygen - new GlyphLayout(game.assets.get("fontHUD.ttf", BitmapFont.class), "Oxygen").width - Gdx.graphics.getWidth()/100, posYOxygen + new GlyphLayout(game.assets.get("fontHUD.ttf", BitmapFont.class), "Oxygen").height); game.batch.draw(skin.getRegion("WhiteSquare"), posXOxygen, posYOxygen, width * hero.getOxygenLevel()/GameConstants.MAX_OXYGEN, height); //Fuel level game.batch.setColor(1,0,0,1); game.assets.get("fontHUD.ttf", BitmapFont.class).draw( game.batch, "Fuel", posXOxygen - new GlyphLayout(game.assets.get("fontHUD.ttf", BitmapFont.class), "Fuel").width - Gdx.graphics.getWidth()/100, posYOxygen + new GlyphLayout(game.assets.get("fontHUD.ttf", BitmapFont.class), "Fuel").height - 2 * height); game.batch.draw(skin.getRegion("WhiteSquare"), posXOxygen, posYOxygen - 2 * height, width * hero.getFuelLevel()/GameConstants.MAX_FUEL, height); } public void win(){ GameConstants.GAME_PAUSED = true; imageTableBackground.setWidth(tableWin.getPrefWidth() + Gdx.graphics.getWidth()/20); imageTableBackground.setHeight(tableWin.getPrefHeight() + Gdx.graphics.getWidth()/20); tableWin.addAction(Actions.alpha(1, 0.25f)); imageTableBackground.addAction(Actions.sequence(Actions.moveTo( Gdx.graphics.getWidth()/2 - imageTableBackground.getWidth()/2, Gdx.graphics.getHeight()/2 - imageTableBackground.getHeight()/2), Actions.alpha(1, 0.25f))); } public void lose(){ GameConstants.GAME_PAUSED = true; loseLabel.setText(loseString); imageTableBackground.setWidth(tableLose.getPrefWidth() + Gdx.graphics.getWidth()/20); imageTableBackground.setHeight(tableLose.getPrefHeight() + Gdx.graphics.getWidth()/20); tableLose.addAction(Actions.alpha(1, 0.25f)); imageTableBackground.addAction(Actions.sequence(Actions.moveTo( Gdx.graphics.getWidth()/2 - imageTableBackground.getWidth()/2, Gdx.graphics.getHeight()/2 - imageTableBackground.getHeight()/2), Actions.alpha(1, 0.25f))); } public void outOfFuel(){ outOfFuelAlpha += 4 * Gdx.graphics.getDeltaTime(); outOfFuelLabel.addAction(Actions.alpha((float)(1 + Math.cos(outOfFuelAlpha))/2)); } public void pause(){ GameConstants.GAME_PAUSED = true; imageTableBackground.setWidth(tablePause.getPrefWidth() + Gdx.graphics.getWidth()/20); imageTableBackground.setHeight(tablePause.getPrefHeight() + Gdx.graphics.getWidth()/20); tablePause.addAction(Actions.alpha(1, 0.25f)); imageTableBackground.addAction(Actions.sequence(Actions.moveTo( Gdx.graphics.getWidth()/2 - imageTableBackground.getWidth()/2, Gdx.graphics.getHeight()/2 - imageTableBackground.getHeight()/2), Actions.alpha(1, 0.25f))); } public void buttonListener(){ nextButton.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y){ game.setScreen(new GameScreen(game)); } }); replayButton.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y){ game.setScreen(new GameScreen(game)); } }); replayButton2.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y){ game.setScreen(new GameScreen(game)); } }); replayButton3.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y){ game.setScreen(new GameScreen(game)); } }); menuButton.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y){ game.setScreen(new MainMenuScreen(game)); } }); menuButton2.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y){ game.setScreen(new MainMenuScreen(game)); } }); resumeButton.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y){ GameConstants.GAME_PAUSED = false; imageTableBackground.addAction(Actions.alpha(0, 0.15f)); tablePause.addAction(Actions.alpha(0, 0.15f)); } }); }
The functions :
In the GameScreen.java :
To use the HUD, we need to add it to the GameScreen.java :
hud = new HUD(game, stage, skin, mapReader.hero);
public void render(float delta) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); camera.displacement(mapReader.hero, tiledMap); camera.update(); if(!GameConstants.GAME_PAUSED){ if(Gdx.input.isKeyPressed(Keys.ESCAPE)){ hud.pause(); } world.step(GameConstants.BOX_STEP, GameConstants.BOX_VELOCITY_ITERATIONS, GameConstants.BOX_POSITION_ITERATIONS); mapReader.active(); if(mapReader.hero.getOxygenLevel() <= 0){ hud.loseString = "OUT OF OXYGEN !"; hud.lose(); } else if (mapReader.hero.getFuelLevel() <= 0) hud.outOfFuel(); } stage.act(); debugRenderer.render(world, camera.combined); //Drawing graphics game.batch.begin(); hud.draw(); game.batch.end(); stage.draw(); //Test Box2DLight rayHandler.setCombinedMatrix(camera); rayHandler.updateAndRender(); }
hud.buttonListener();
We also need to modify the ContactListener to display winTable and loseTable in the corresponding situation :
if(fixtureA.getUserData() != null && fixtureB.getUserData() != null) { //Finish the level if(fixtureA.getUserData().equals("Tom") && fixtureB.getUserData().equals("Exit")) hud.win(); else if(fixtureB.getUserData().equals("Tom") && fixtureA.getUserData().equals("Exit")) hud.win(); }
public void postSolve(Contact contact, ContactImpulse impulse) { Body bodyA = contact.getFixtureA().getBody(); Body bodyB = contact.getFixtureB().getBody(); //Hero death by crushing if(bodyA.getUserData().equals("Tom") || bodyB.getUserData().equals("Tom")){ for(int i = 0; i < impulse.getNormalImpulses().length; i++){ if(impulse.getNormalImpulses()[i] > GameConstants.CRUSH_IMPULSE){ hud.loseString = "CRUSHED !"; hud.lose(); } } } }
And that's it! We now have a HUD!
Until now, the only output I have in my game is the console. If I get crushed, if I'm out of fuel or oxygen, or if I finish the level, I have to check the console. I need a HUD to display informations on the GameScreen.
I want the HUD to display the oxygen and fuel levels, to display a menu if the player wins or loses. I want something like this :
For that I created a HUD class.
Here is the code for the HUD.java creator :
public class HUD { final MyGdxGame game; public Image OxygenBar, FuelBar; private float posXOxygen, posYOxygen, width, height, outOfFuelAlpha; private Hero hero; private Table tableWin, tableLose, tablePause; private TextButtonStyle textButtonStyle; private TextButton nextButton, replayButton, replayButton2, replayButton3, menuButton, menuButton2, resumeButton; private LabelStyle menulabelStyle, hudLabelStyle; private Label outOfFuelLabel, loseLabel; private Image imageTableBackground; public String loseString; public HUD(final MyGdxGame game, Stage stage, Skin skin, Hero hero){ this.game = game; this.hero = hero; outOfFuelAlpha = 0; posXOxygen = 9 * Gdx.graphics.getWidth()/100; posYOxygen = 95 * Gdx.graphics.getHeight()/100; width = Gdx.graphics.getWidth()/3; height = Gdx.graphics.getHeight()/70; loseString = "You lost !"; menulabelStyle = new LabelStyle(game.assets.get("fontMenu.ttf", BitmapFont.class), Color.WHITE); hudLabelStyle = new LabelStyle(game.assets.get("fontHUD.ttf", BitmapFont.class), Color.WHITE); outOfFuelLabel = new Label("PRESS ESC TO RESTART", hudLabelStyle); outOfFuelLabel.setX(Gdx.graphics.getWidth()/2 - new GlyphLayout(game.assets.get("fontHUD.ttf", BitmapFont.class), outOfFuelLabel.getText()).width/2); outOfFuelLabel.setY(Gdx.graphics.getHeight()/2 - new GlyphLayout(game.assets.get("fontHUD.ttf", BitmapFont.class), outOfFuelLabel.getText()).height/2); outOfFuelLabel.addAction(Actions.alpha(0)); loseLabel = new Label(loseString, menulabelStyle); textButtonStyle = new TextButtonStyle(); textButtonStyle.up = skin.getDrawable("Button"); textButtonStyle.down = skin.getDrawable("ButtonChecked"); textButtonStyle.font = game.assets.get("fontTable.ttf", BitmapFont.class); textButtonStyle.fontColor = Color.WHITE; //Win table buttons nextButton = new TextButton("NEXT", textButtonStyle); replayButton = new TextButton("PLAY AGAIN", textButtonStyle); //Lose table buttons replayButton2 = new TextButton("PLAY AGAIN", textButtonStyle); menuButton = new TextButton("MENU", textButtonStyle); //Pause table buttons replayButton3 = new TextButton("PLAY AGAIN", textButtonStyle); menuButton2 = new TextButton("MENU", textButtonStyle); resumeButton = new TextButton("RESUME", textButtonStyle); tableWin = new Table(); tableWin.setFillParent(true); tableWin.row().colspan(2); tableWin.add(new Label("LEVEL CLEARED", menulabelStyle)).padBottom(Gdx.graphics.getHeight()/22); tableWin.row().width(Gdx.graphics.getWidth()/4); tableWin.add(nextButton).spaceRight(Gdx.graphics.getWidth()/100); tableWin.add(replayButton); tableWin.addAction(Actions.alpha(0)); tableLose = new Table(); tableLose.setFillParent(true); tableLose.row().colspan(2); tableLose.add(loseLabel).padBottom(Gdx.graphics.getHeight()/22); tableLose.row().width(Gdx.graphics.getWidth()/4); tableLose.add(replayButton2).spaceRight(Gdx.graphics.getWidth()/100); tableLose.add(menuButton); tableLose.addAction(Actions.alpha(0)); tablePause = new Table(); tablePause.setFillParent(true); tablePause.add(resumeButton).width(replayButton3.getPrefWidth()).pad(Gdx.graphics.getHeight()/50).row(); tablePause.add(replayButton3).width(replayButton3.getPrefWidth()).pad(Gdx.graphics.getHeight()/50).row(); tablePause.add(menuButton2).width(replayButton3.getPrefWidth()).pad(Gdx.graphics.getHeight()/50); tablePause.addAction(Actions.alpha(0)); imageTableBackground = new Image(skin.getDrawable("imageTable")); imageTableBackground.setColor(0,0,0.25f,1); imageTableBackground.setWidth(1.15f*tableWin.getPrefWidth()); imageTableBackground.setHeight(1.15f*tableWin.getPrefHeight()); imageTableBackground.addAction(Actions.alpha(0)); stage.addActor(imageTableBackground); stage.addActor(tableWin); stage.addActor(tableLose); stage.addActor(tablePause); stage.addActor(outOfFuelLabel); } }
In the creator :
The Asset Manager has been set up, and some fonts and sprites sheets were loaded during the loading screen. Now I can create a MainMenuScreen.
The MainMenuScreen code is very simple. Basically, the screen is the same as the LoadingScreen, but it will also display a "Play" button :
Here is the code of the MainMenuScreen.java :
public class MainMenuScreen implements Screen{ final MyGdxGame game; private OrthographicCamera camera; private Stage stage; private Skin skin; private Texture textureLogo; private Image imageLogo; private TextureAtlas textureAtlas; private TextButton playButton, optionButton; private TextButtonStyle textButtonStyle; public MainMenuScreen(final MyGdxGame game){ this.game = game; camera = new OrthographicCamera(); camera.setToOrtho(false, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); textureLogo = new Texture(Gdx.files.internal("Images/Logo.jpg"), true); textureLogo.setFilter(TextureFilter.MipMapLinearNearest, TextureFilter.MipMapLinearNearest); imageLogo = new Image(textureLogo); imageLogo.setWidth(Gdx.graphics.getWidth()); imageLogo.setHeight(textureLogo.getHeight() * imageLogo.getWidth()/textureLogo.getWidth()); imageLogo.setX(Gdx.graphics.getWidth()/2 - imageLogo.getWidth()/2); imageLogo.setY(Gdx.graphics.getHeight()/2 - imageLogo.getHeight()/2); stage = new Stage(); skin = new Skin(); textureAtlas = game.assets.get("Images/Images.pack", TextureAtlas.class); skin.addRegions(textureAtlas); textButtonStyle = new TextButtonStyle(); textButtonStyle.up = skin.getDrawable("Button"); textButtonStyle.down = skin.getDrawable("ButtonChecked"); textButtonStyle.font = game.assets.get("fontMenu.ttf", BitmapFont.class); textButtonStyle.fontColor = Color.WHITE; textButtonStyle.downFontColor = new Color(0, 0, 0, 1); playButton = new TextButton("PLAY", textButtonStyle); playButton.setHeight(Gdx.graphics.getHeight()/7); playButton.setX(Gdx.graphics.getWidth()/2 - playButton.getWidth()/2); playButton.setY(29 * Gdx.graphics.getHeight()/100 - playButton.getHeight()/2); stage.addActor(imageLogo); stage.addActor(playButton); playButton.addAction(Actions.sequence(Actions.alpha(0) ,Actions.fadeIn(0.25f))); } @Override public void render(float delta) { Gdx.gl.glClearColor(0,0,0,1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); game.batch.setProjectionMatrix(camera.combined); stage.act(); stage.draw(); } @Override public void show() { Gdx.input.setInputProcessor(stage); playButton.addListener(new ClickListener(){ @Override public void clicked(InputEvent event, float x, float y) { game.setScreen(new GameScreen(game)); } }); } @Override public void dispose() { this.dispose(); stage.dispose(); } }
In the creator :
In the render() :
In the show():
In the dispose(): Like on EVERY screens, we need to dipose everything that can be diposed, to avoid memory leak.
And that's it for the main menu screen !
Before creating you fonts, you need these lines :
FileHandleResolver resolver = new InternalFileHandleResolver(); game.assets.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(resolver)); game.assets.setLoader(BitmapFont.class, ".ttf", new FreetypeFontLoader(resolver));
They allow the Asset Manager to load the .ttf file and generate the FreeTypeFont based on the parameters you'll use.
Then you create the parameter :
FreeTypeFontLoaderParameter size1Params = new FreeTypeFontLoaderParameter();
The parameter is composed of the font file ( .ttf) you put in you Asset folder :
size1Params.fontFileName = "Fonts/good times rg.ttf";
Then you can to this parameter a filter to have very smooth font :
size1Params.fontParameters.genMipMaps = true; size1Params.fontParameters.minFilter = TextureFilter.Linear; size1Params.fontParameters.magFilter = TextureFilter.Linear;
And finally, you chose a size for your font :
size1Params.fontParameters.size = Gdx.graphics.getWidth()/18;
Notice that the size is dependant on the screen size, which is all the interest of using FreeTypeFonts. The size of this font relative to the screen size will be the same on every screens.
Then, all that remains is loading the font in the Asset Manager :
game.assets.load("fontMenu.ttf", BitmapFont.class, size1Params);
Notice that you can chose the name you want at this point for your font. In this case I chose "fontMenu.ttf", thus when I'll need that font, I'll do :
game.assets.get("fontMenu.ttf", BitmapFont.class)
And that's it ! You can create the font of any size you want by this method !
OK now, I have a nice logo, I can create a main menu screen that will display this logo, and at least a button that you have to press to start playing. Eventually, I'll add an "Option" button and a "Quit" button.
Before creating the main menu screen, I need to create few assets, just to give a nice look to the buttons.
For that I'll use 2 tools :
Creating NinePatch
What is a NinePatch and why do we need NinePatchs ? A NinePatch is an image that you can stretch along X and Y axis in order to fill a region. It's very useful to create nice user interface, for example to skin buttons. The NinePatch is divided in 9 regions, among which 5 are scalable while the 4 regions in the corner will keep their size and proportions. Here is an illustration that shows the difference between scaling a NinePatch vs scaling a normal image.
Thus, I created my first assets, composed of 4 NinePatchs, with the tool draw9patch (that you'll find in you SDK tool folder), with which I will create my UI.
The 2 big images will be used for buttons. There is one picture for the button in initial state, and one that will be used when the button is pressed. The small square will be used for basic representation of the oxygen and fuel levels during the game, and the last one will be used as background for various Tables.
Creating the TextureAtlas
Now that I have my first assets, I need to pack them in a single png image with the Texture Packer that you can download here. Packing all the pictures in a single file will optimize the GPU usage : You load the big picture only once, then you draw only the portion you need.
Once you packed your assets, you obtain two files : the .png file that contains every picture you packed, and a .pack file, that is a text file containing the name and the coordinate of all the pictures. Therefore, in your code you'll access every single picture by it's name.
Now that we have assets, we need an Asset Manager ! For the Asset Manager , I create it in the loading screen.
Creating the loading screen
Actually, we already have the Asset Manager , as I created it in the Main activity, remember :
public class MyGdxGame extends Game implements ApplicationListener{ public SpriteBatch batch; public AssetManager assets; @Override public void create () { batch = new SpriteBatch(); assets = new AssetManager(); this.setScreen(new GameScreen(this)); } @Override public void render () { super.render(); } }
Finally, we create the loading screen, in which we'll load a lot of things in the Asset Manager. This screen will be displayed only during the loading time. Thus, with a very small image to load, the screen will appear during less that one second, but when we'll have a lot of pictures in our Texture Atlas, it will take more time.
During the loading screen, I load the Texture Atlas, and I create and load several fonts that will be used during the game.
During the loading time, the screen will display the nice logo I created yesterday.
Important : every asset (image, texture atlas, font file, sound, level maps...) must be stored in the folder Android ---> Asset. In the Asset folder, I create subfolders Image, Sound, Fonts. Every time you put an asset or modify in the Asset folder, you need to refresh the Android folder in Eclipse.
Here is the code for the LoadingScreen.java :
public class LoadingScreen implements Screen{ final MyGdxGame game; OrthographicCamera camera; private Texture textureLogo; private Image imageLogo; private Stage stage; public LoadingScreen(final MyGdxGame game){ this.game = game; camera = new OrthographicCamera(); camera.setToOrtho(false, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); //Creating the logo picture textureLogo = new Texture(Gdx.files.internal("Images/Logo.jpg"), true); textureLogo.setFilter(TextureFilter.MipMapLinearNearest, TextureFilter.MipMapLinearNearest); imageLogo = new Image(textureLogo); imageLogo.setWidth(Gdx.graphics.getWidth()); imageLogo.setHeight(textureLogo.getHeight() * imageLogo.getWidth()/textureLogo.getWidth()); imageLogo.setX(Gdx.graphics.getWidth()/2 - imageLogo.getWidth()/2); imageLogo.setY(Gdx.graphics.getHeight()/2 - imageLogo.getHeight()/2); stage = new Stage(); //Loading of the TextureAtlas game.assets.load("Images/Images.pack", TextureAtlas.class); //Loading of the Freetype Fonts FileHandleResolver resolver = new InternalFileHandleResolver(); game.assets.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(resolver)); game.assets.setLoader(BitmapFont.class, ".ttf", new FreetypeFontLoader(resolver)); FreeTypeFontLoaderParameter size1Params = new FreeTypeFontLoaderParameter(); size1Params.fontFileName = "Fonts/good times rg.ttf"; size1Params.fontParameters.genMipMaps = true; size1Params.fontParameters.minFilter = TextureFilter.Linear; size1Params.fontParameters.magFilter = TextureFilter.Linear; size1Params.fontParameters.size = Gdx.graphics.getWidth()/18; game.assets.load("fontMenu.ttf", BitmapFont.class, size1Params); FreeTypeFontLoaderParameter size2Params = new FreeTypeFontLoaderParameter(); size2Params.fontFileName = "Fonts/good times rg.ttf"; size2Params.fontParameters.genMipMaps = true; size2Params.fontParameters.minFilter = TextureFilter.Linear; size2Params.fontParameters.magFilter = TextureFilter.Linear; size2Params.fontParameters.size = Gdx.graphics.getWidth()/35; game.assets.load("fontTable.ttf", BitmapFont.class, size2Params); FreeTypeFontLoaderParameter size3Params = new FreeTypeFontLoaderParameter(); size3Params.fontFileName = "Fonts/good times rg.ttf"; size3Params.fontParameters.genMipMaps = true; size3Params.fontParameters.minFilter = TextureFilter.Linear; size3Params.fontParameters.magFilter = TextureFilter.Linear; size3Params.fontParameters.size = 13 * Gdx.graphics.getWidth()/1000; game.assets.load("fontHUD.ttf", BitmapFont.class, size3Params); //Displaying the logo picture stage.addActor(imageLogo); imageLogo.addAction(Actions.sequence(Actions.alpha(0) ,Actions.fadeIn(0.1f),Actions.delay(1.5f))); } @Override public void render(float delta) { Gdx.gl.glClearColor(1, 1, 1, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); camera.update(); game.batch.setProjectionMatrix(camera.combined); stage.act(); stage.draw(); if(game.assets.update()) ((Game)Gdx.app.getApplicationListener()).setScreen(new MainMenuScreen(game)); } @Override public void dispose() { stage.dispose(); } }
About this code :
During the first week I coded most of the game mechanics. Now it's time work on the visual identity of the game. Before working on the game graphics, I worked on the logo, that will be the main menu screen background. And to make that logo, I needed a name.
After one week, my project still didn't have a name. Thus, I took benefit of the past few days without coding, due to Christmas time, to think about a name.
The first name that came up was simply COSMONAUT. I like simple things. Always. But, I wondered... should I find a name that says more about the game. Should I find a name more intriguing ? Should I use something like "Lost in Space", "Forlorn", "Abandoned", "Condemned" ?
Well... I found all that boring as hell, I finally stuck to my first choice, which is almost a rule of life with me, and I kept the simple COSMONAUT. I really like it. It's very simple, it doesn't say anything about the gameplay and the story, it only says about who you are, a cosmonaut.
Then I spent a bit of time on Photoshop to create this logo :
For those who are interested in the process of creation of this logo, here is a speed-up video of the whole process :
The jam started last Friday, and it was a very productive week. I worked only on the code for during this week, and left aside my weaknesses, say drawing and designing sounds.
During these two days spent celebrating Christmas, I put my work on the libGDX Jam on hold. But I think it's a good time to make a little review of what I've done so far, and what I still must do.
That's still A LOT to do !
Merry Christmas everyone !
All that is missing in this world is a little bit of hope ! During is travel, Major Tom will find oxygen and fuel refill, that will save his life, more that once.
Thus I created the OxygenRefill and FuelRefill classes, that are subclasses of an Item class.
Here is the code of the Item.java :
public class Item { protected static World world; public Body body; private BodyDef bodyDef; private FixtureDef fixtureDef; private PolygonShape polygonShape; private float width, height; public boolean used; public Item(){ } public void create(World world, OrthographicCamera camera, MapObject mapObject){ this.world = world; used = false; width = mapObject.getProperties().get("width", float.class)/2 * GameConstants.MPP; height = mapObject.getProperties().get("height", float.class)/2 * GameConstants.MPP; bodyDef = new BodyDef(); fixtureDef = new FixtureDef(); bodyDef.type = BodyType.DynamicBody; bodyDef.position.set((mapObject.getProperties().get("x", float.class) + mapObject.getProperties().get("width", float.class)/2) * GameConstants.MPP, (mapObject.getProperties().get("y", float.class) + 1.5f*mapObject.getProperties().get("height", float.class)) * GameConstants.MPP); polygonShape = new PolygonShape(); polygonShape.setAsBox(width, height); fixtureDef.shape = polygonShape; fixtureDef.density = 0.1f; fixtureDef.friction = 0.2f; fixtureDef.restitution = 0f; fixtureDef.isSensor = true; body = world.createBody(bodyDef); body.createFixture(fixtureDef).setUserData("Item"); body.setUserData("Item"); } public void activate(){ //Called when Major Tom collide with the item } public void active(TiledMapReader tiledMapReader){ if(used){ body.setActive(false); world.destroyBody(body); tiledMapReader.items.removeIndex(tiledMapReader.items.indexOf(this, true)); } } }
About this code :
FuelRefill.java :
public class FuelRefill extends Item{ private static Hero hero; public FuelRefill(World world, OrthographicCamera camera, MapObject mapObject, Hero hero){ this.hero = hero; create(world, camera, mapObject); } @Override public void activate(){ used = true; System.out.println("Fuel level before refill : " + hero.getFuelLevel()); hero.setFuelLevel(hero.getFuelLevel() + GameConstants.FUEL_REFILL); if(hero.getFuelLevel() > GameConstants.MAX_FUEL) hero.setFuelLevel(GameConstants.MAX_FUEL); System.out.println("Fuel level after refill : " + hero.getFuelLevel()); } }
OxygenRefill.java :
public class OxygenRefill extends Item{ private static Hero hero; public OxygenRefill(World world, OrthographicCamera camera, MapObject mapObject, Hero hero){ this.hero = hero; create(world, camera, mapObject); } @Override public void activate(){ used = true; System.out.println("Oxygen level before refill : " + hero.getOxygenLevel()); hero.setOxygenLevel(hero.getFuelLevel() + GameConstants.OXYGEN_REFILL); if(hero.getOxygenLevel() > GameConstants.MAX_OXYGEN) hero.setOxygenLevel(GameConstants.MAX_OXYGEN); System.out.println("Oxygen level after refill : " + hero.getOxygenLevel()); } }
As you can see, these 2 subclasses are very straightforward. We only define the activate() function, to add either fuel or oxygen. A couple of "System.out.println()" are here only to monitor the fuel and oxygen level in the console, ash I still didn't create the HUD.
All we need to make the activate() function run is adding these two lines in the GameConstants.java :
public static float FUEL_REFILL = 40f; public static float OXYGEN_REFILL = 30f;
Of course, these values are arbitrary for now. I'll do the fine tuning much later.
Recognizing items with the TiledMapReader.java :
This happens in the same for loop as the Switches recognition, as the items will be placed in the "Spawn" layer of the Tiled Map, and not in the "Object" layer. Processing like that will make it easier to visualise things when I'll create levels in the level editor.Therefore, this for loop that reads the Spawn layer looks like that now :
//Spawned items for(int i = 0; i < tiledMap.getLayers().get("Spawn").getObjects().getCount(); i++){ if(tiledMap.getLayers().get("Spawn").getObjects().get(i).getProperties().get("Type") != null){ //Switches if(tiledMap.getLayers().get("Spawn").getObjects().get(i).getProperties().get("Type").equals("Switch")){ ItemSwitch itemSwitch = new ItemSwitch(world, camera, tiledMap.getLayers().get("Spawn").getObjects().get(i)); switchs.add(itemSwitch); } //Oxygen Refill else if(tiledMap.getLayers().get("Spawn").getObjects().get(i).getProperties().get("Type").equals("Oxygen")){ OxygenRefill oxygenRefill = new OxygenRefill(world, camera, tiledMap.getLayers().get("Spawn").getObjects().get(i), hero); items.add(oxygenRefill); } //Fuel Refill else if(tiledMap.getLayers().get("Spawn").getObjects().get(i).getProperties().get("Type").equals("Fuel")){ FuelRefill fuelRefill = new FuelRefill(world, camera, tiledMap.getLayers().get("Spawn").getObjects().get(i), hero); items.add(fuelRefill); } }
And I added a new function in the TiledMapReader.java :
public void active(){ hero.displacement(); for(Obstacle obstacle : obstacles) obstacle.active(); for(Item item: items) item.active(this); }
This function will run in the render() of the GameScreen.java and it will replace this lines
mapReader.hero.displacement(); for(Obstacle obstacle : mapReader.obstacles){ obstacle.active();
by this line
mapReader.active();
Finally, we only need to update the beginContact function of the ContactListener, in the GameScreen :
public void beginContact(Contact contact) { Body bodyA = contact.getFixtureA().getBody(); Body bodyB = contact.getFixtureB().getBody(); Fixture fixtureA = contact.getFixtureA(); Fixture fixtureB = contact.getFixtureB(); if(fixtureA.getUserData() != null && fixtureB.getUserData() != null) { ... //Items if(fixtureA.getUserData().equals("Tom") && fixtureB.getUserData().equals("Item")){ for(Item item : mapReader.items){ if(item.body == fixtureB.getBody()) item.activate(); } } else if(fixtureB.getUserData().equals("Tom") && fixtureA.getUserData().equals("Item")){ for(Item item : mapReader.items){ if(item.body == fixtureA.getBody()) item.activate(); } } } }
And here is the result !
So here is the (short and easy) code for this ObstacleRevolving.java :
public class ObstacleRevolving extends Obstacle{ private float speed = 90; public ObstacleRevolving(World world, OrthographicCamera camera, MapObject rectangleObject) { super(world, camera, rectangleObject); //Rotation speed if(rectangleObject.getProperties().get("Speed") != null) speed = Float.parseFloat((String) rectangleObject.getProperties().get("Speed")); body.setFixedRotation(false); body.setAngularVelocity(speed*MathUtils.degreesToRadians); } @Override public BodyType getBodyType(){ return BodyType.KinematicBody; } @Override public void activate(){ active = !active; if(active) body.setAngularVelocity(speed*MathUtils.degreesToRadians); else body.setAngularVelocity(0); } }
About this code :
for (RectangleMapObject rectangleObject : objects.getByType(RectangleMapObject.class)) { if(rectangleObject.getProperties().get("Type") != null){ ... //Revolving obstacles else if(rectangleObject.getProperties().get("Type").equals("Revolving")){ ObstacleRevolving obstacle = new ObstacleRevolving(world, camera, rectangleObject); obstacles.add(obstacle); } ... }And here is the result !
Now that I can control objects with switches, I can create doors, that can be open/closed with switches.
Here is the code of ObstacleDoor.java :
public class ObstacleDoor extends Obstacle{ private float speed = 5; private Vector2 initialPosition, finalPosition; public ObstacleDoor(World world, OrthographicCamera camera, MapObject rectangleObject) { super(world, camera, rectangleObject); //Motion speed if(rectangleObject.getProperties().get("Speed") != null){ speed = Float.parseFloat((String) rectangleObject.getProperties().get("Speed")); } initialPosition = new Vector2(posX, posY); if(width > height) finalPosition = new Vector2(posX + Math.signum(speed) * 1.9f*width, posY); else finalPosition = new Vector2(posX, posY + Math.signum(speed) * 1.9f*height); } @Override public BodyType getBodyType(){ return BodyType.KinematicBody; } @Override public void active(){ if(active) body.setLinearVelocity( Math.signum(speed) * (initialPosition.x - body.getPosition().x) * speed, Math.signum(speed) * (initialPosition.y - body.getPosition().y) * speed ); else body.setLinearVelocity( Math.signum(speed) * (finalPosition.x - body.getPosition().x) * speed, Math.signum(speed) * (finalPosition.y - body.getPosition().y) * speed ); } @Override public void activate(){ active = !active; } }
About this code :
The TiledMapReader.java need to recognize the ObstacleDoor in the map :
for (RectangleMapObject rectangleObject : objects.getByType(RectangleMapObject.class)) { if(rectangleObject.getProperties().get("Type") != null){ ... //Doors else if(rectangleObject.getProperties().get("Type").equals("Door")){ ObstacleDoor obstacle = new ObstacleDoor(world, camera, rectangleObject); obstacles.add(obstacle); } ... } }
And here is the result !
OK, thus far I have animated obstacles like ObstaclePiston and ObstacleMoving, and I plan to have few others. It would be very interesting if we could enable/disable these animated obstacles with switches. And it would be interesting if one switch could control several obstacles at a time, and also if an obstacle could be controlled by several switches. It will allow me to create some puzzles.
Here is a video showing how that works :
As you can see in the video, to associate a switch with one or several animated object, I use an Association Number. For that, as usual, I set a property in Tiled, I give it the name "Association Number", and I give the same number to the switch and the animated object that I want to control. I can give several Association Numbers to the switch, by separating them with a comma, if I want the switch to control several objects.
Here is the code for ItemSwitch.java :
public class ItemSwitch { public Body swtichBody; private BodyDef bodyDef; private FixtureDef fixtureDef; private PolygonShape switchShape; private float width, height; private boolean isOn; private String[] associationNumbers; public ItemSwitch(World world, OrthographicCamera camera, MapObject mapObject){ create(world, camera, mapObject); } public void create(World world, OrthographicCamera camera, MapObject mapObject){ //Is the switch on ? if(mapObject.getProperties().get("On") != null){ if(Integer.parseInt((String) mapObject.getProperties().get("On")) == 1) isOn = true; else isOn = false; } else isOn = false; //Association Numbers if(mapObject.getProperties().get("Association Number") != null){ associationNumbers = mapObject.getProperties().get("Association Number").toString().split(","); } width = mapObject.getProperties().get("width", float.class)/2 * GameConstants.MPP; height = mapObject.getProperties().get("height", float.class)/2 * GameConstants.MPP; bodyDef = new BodyDef(); fixtureDef = new FixtureDef(); bodyDef.type = BodyType.StaticBody; bodyDef.position.set((mapObject.getProperties().get("x", float.class) + mapObject.getProperties().get("width", float.class)/2) * GameConstants.MPP, (mapObject.getProperties().get("y", float.class) + mapObject.getProperties().get("height", float.class)) * GameConstants.MPP); switchShape = new PolygonShape(); switchShape.setAsBox(width, height); fixtureDef.shape = switchShape; fixtureDef.density = 0; fixtureDef.friction = 0.2f; fixtureDef.restitution = 0f; fixtureDef.isSensor = true; swtichBody = world.createBody(bodyDef); swtichBody.createFixture(fixtureDef).setUserData("Switch"); swtichBody.setUserData("Switch"); } public void active(Array<Obstacle> obstacles){ isOn = !isOn; for(String number : associationNumbers){ for(Obstacle obstacle : obstacles) if(obstacle.associationNumber == Integer.valueOf(number)) obstacle.activate(); } } }
About this code :
To use the ItemSwitch, with the Obstacles, I need to do some modifications in Obstacle.java:
The activate() function will be defined in each type of Obstacle. For now it the same function for both ObstaclePiston and ObstacleMoving :
public void activate(){ active = !active; }And of course, the active() function of every type of Obstacle must take into account the new boolean active (I guess it starts to be really confusing between active, active() and activate()) So basicaly, the new active() of every Obstacle looks like this :
public void active(){ if(active){ //Do the regular stuff } else body.setLinearVelocity(0, 0); }
Finally, we need to detect collision between Major Tom and ItemSwitch
public void beginContact(Contact contact) { Fixture fixtureA = contact.getFixtureA(); Fixture fixtureB = contact.getFixtureB(); if(fixtureA.getUserData() != null && fixtureB.getUserData() != null) { //Switch if(fixtureA.getUserData().equals("Tom") && fixtureB.getUserData().equals("Switch")){ for(ItemSwitch itemSwitch : mapReader.switchs){ if(itemSwitch.swtichBody == fixtureB.getBody()) itemSwitch.active(mapReader.obstacles); } } else if(fixtureB.getUserData().equals("Tom") && fixtureA.getUserData().equals("Switch")){ for(ItemSwitch itemSwitch : mapReader.switchs){ if(itemSwitch.swtichBody == fixtureA.getBody()) itemSwitch.active(mapReader.obstacles); } } } }
And that's it ! Here is the result :
Here is the situation : Major Tom’s spaceship is completely wrecked because it entered a very dense region of the asteroid belt, thus it was hit by uncountable micrometeoroids. In some places, tubes carrying oxygen a various gases were punctured, causing gas leak.
In the absence of gravity, these gas leaks will propel anything that crosses the gas spray.
Here is an animation showing what happens when Major Tom pushes floating boxes in a gas leak :
Let's see the code of Leak.java :
public class Leak extends Obstacle{ private Set<Fixture> fixtures; private Vector2 leakForce, leakOrigin; private float force, leakSize; public Leak(World world, OrthographicCamera camera, MapObject rectangleObject) { super(world, camera, rectangleObject); body.getFixtureList().get(0).setSensor(true); body.getFixtureList().get(0).setUserData("Leak"); body.setUserData("Leak"); fixtures = new HashSet<Fixture>(); //Leak force if(rectangleObject.getProperties().get("Force") != null){ force = Float.parseFloat(rectangleObject.getProperties().get("Force").toString()) * GameConstants.DEFAULT_LEAK_FORCE; } else force = GameConstants.DEFAULT_LEAK_FORCE; //Leak direction and leak origine if(rectangle.width > rectangle.height){ leakForce = new Vector2(force, 0); leakSize = rectangle.width * GameConstants.MPP; if(force > 0) leakOrigin = new Vector2(posX - width, posY); else leakOrigin = new Vector2(posX + width, posY); } else{ leakForce = new Vector2(0, force); leakSize = rectangle.height * GameConstants.MPP; if(force > 0) leakOrigin = new Vector2(posX, posY - height); else leakOrigin = new Vector2(posX, posY + height); } } public void addBody(Fixture fixture) { PolygonShape polygon = (PolygonShape) fixture.getShape(); if (polygon.getVertexCount() > 2) fixtures.add(fixture); } public void removeBody(Fixture fixture) { fixtures.remove(fixture); } public void active(){ for(Fixture fixture : fixtures){ float distanceX = Math.abs(fixture.getBody().getPosition().x - leakOrigin.x); float distanceY = Math.abs(fixture.getBody().getPosition().y - leakOrigin.y); fixture.getBody().applyForceToCenter( leakForce.x * Math.abs(leakSize - distanceX)/leakSize, leakForce.y * Math.abs(leakSize - distanceY)/leakSize, true ); } } }
About this code :
for (RectangleMapObject rectangleObject : objects.getByType(RectangleMapObject.class)) {
if(rectangleObject.getProperties().get("Type") != null){
...
//Leaks
else if(rectangleObject.getProperties().get("Type").equals("Leak")){
Leak leak = new Leak(world, camera, rectangleObject);
obstacles.add(leak);
}
...
}
}
In the GameScreen.java, we'll use beginContact and enContact functions of the ContactListener to add or remove bodies from the gas spray :
public void beginContact(Contact contact) { Fixture fixtureA = contact.getFixtureA(); Fixture fixtureB = contact.getFixtureB(); if(fixtureA.getUserData() != null && fixtureB.getUserData() != null) { //Leak if (fixtureA.getUserData().equals("Leak") && fixtureB.getBody().getType() == BodyType.DynamicBody) { for(Obstacle obstacle : mapReader.obstacles){ if(obstacle.body.getFixtureList().get(0) == fixtureA){ Leak leak = (Leak) obstacle; leak.addBody(fixtureB); } } } else if (fixtureB.getUserData().equals("Leak") && fixtureA.getBody().getType() == BodyType.DynamicBody) { for(Obstacle obstacle : mapReader.obstacles){ if(obstacle.body.getFixtureList().get(0) == fixtureB){ Leak leak = (Leak) obstacle; leak.addBody(fixtureA); } } } } } @Override public void endContact(Contact contact) { Fixture fixtureA = contact.getFixtureA(); Fixture fixtureB = contact.getFixtureB(); if(fixtureA.getUserData() != null && fixtureB.getUserData() != null) { //Leak if (fixtureA.getUserData().equals("Leak") && fixtureB.getBody().getType() == BodyType.DynamicBody) { for(Obstacle obstacle : mapReader.obstacles){ if(obstacle.body.getFixtureList().get(0) == fixtureA){ Leak leak = (Leak) obstacle; leak.removeBody(fixtureB); } } } else if (fixtureB.getUserData().equals("Leak") && fixtureA.getBody().getType() == BodyType.DynamicBody) { for(Obstacle obstacle : mapReader.obstacles){ if(obstacle.body.getFixtureList().get(0) == fixtureB){ Leak leak = (Leak) obstacle; leak.removeBody(fixtureA); } } } } }
And that's it ! Another feature for the level design !
The ObstacleMoving is an obstacle that will follow a given path. You only have to draw the path in Tiled, and the code will generate an Obstacle that will follow this path, back and forth, or in a loop according to the properties you give it.
To draw the path, we'll use the polyline tool of Tiled :
And here is an animation showing how easy it is to create an ObstacleMoving... once you typed all the code, haha.
Modifying the Obstacle.java
First, obviously, this ObstacleMoving requires a PolylineMapObject instead of a RectangleMapObject, thus we can't use the Obstacle.java as is. We need to add a creator that takes into account the PolylineMapObject.
Here is the new creator in Obstacle.java :
public Obstacle(World world, OrthographicCamera camera, PolylineMapObject polylineObject){ }
Yes, this creator is empty. It's only here in order to be able to create a subclass, ObstacleMoving.java, that uses a PolylineMapObject.
And here is the code of the ObstacleMoving.java :
public class ObstacleMoving extends Obstacle{ private float speed; private boolean backward, loop; private Vector2 direction; private Vector2[] path; private int step; public ObstacleMoving(World world, OrthographicCamera camera, PolylineMapObject polylineObject) { super(world, camera, polylineObject); //SPEED if(polylineObject.getProperties().get("Speed") != null) speed = Float.parseFloat((String) polylineObject.getProperties().get("Speed")); else speed = 5; //DOES THE PATH MAKE A LOOP ? if(polylineObject.getProperties().get("Loop") != null) loop = true; else loop = false; //WIDTH OF THE MOVING OBJECT if(polylineObject.getProperties().get("Width") != null) width = Integer.parseInt((String) polylineObject.getProperties().get("Width")) * GameConstants.PPT * GameConstants.MPP/2; else width = 2 * GameConstants.PPT * GameConstants.MPP/2; //HEIGHT OF THE MOVING OBJECT if(polylineObject.getProperties().get("Height") != null) height = Integer.parseInt((String) polylineObject.getProperties().get("Height")) * GameConstants.PPT * GameConstants.MPP/2; else height = 2 * GameConstants.PPT * GameConstants.MPP/2; path = new Vector2[polylineObject.getPolyline().getTransformedVertices().length/2]; for(int i = 0; i < path.length; i++){ path[i] = new Vector2(polylineObject.getPolyline().getTransformedVertices()[i*2]*GameConstants.MPP, polylineObject.getPolyline().getTransformedVertices()[i*2 + 1]*GameConstants.MPP); } polygonShape = new PolygonShape(); polygonShape.setAsBox(width, height); bodyDef = new BodyDef(); bodyDef.type = getBodyType(); bodyDef.position.set(path[0]); fixtureDef = new FixtureDef(); fixtureDef.shape = polygonShape; fixtureDef.density = 0.0f; fixtureDef.friction = 0.0f; fixtureDef.restitution = 0f; body = world.createBody(bodyDef); body.createFixture(fixtureDef).setUserData("Objet"); body.setUserData("Objet"); polygonShape.dispose(); direction = new Vector2(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y); body.setLinearVelocity(direction.clamp(speed, speed)); } @Override public BodyType getBodyType(){ return BodyType.KinematicBody; } @Override public void active(){ if(!loop){ if(!backward){ if(!new Vector2(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y).hasSameDirection(direction)){ step++; if(step == path.length){ backward = true; step = path.length - 2; } direction.set(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y); } } else{ if(!new Vector2(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y).hasSameDirection(direction)){ step--; if(step < 0){ backward = false; step = 1; } direction.set(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y); } } } else{ if(!new Vector2(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y).hasSameDirection(direction)){ step++; if(step == path.length){ step = 0; } direction.set(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y); } } body.setLinearVelocity(direction.clamp(speed, speed)); } }
About this code :
OK now we have these nice pistons, Major Tom can get crushed by them, making the player lose the game.
Detecting the event "Tom gets crushed" happens in the ContactListener we set in the GameScreen, to detect collisions between bodies.
Remember, the ContactListener has 4 methods :
The one that interests us here is the postSolve method. The postSolve method gives us access to the impulse that a body undergoes after a collision. The idea is that the more the hero is crushed, the higher the impulse he undergoes. Thus, if the impulse exceeds a predetermined value, the hero dies.
Here is the code of the postSolve method of the ContactListener in the GameScreen.java :
public void postSolve(Contact contact, ContactImpulse impulse) { Body bodyA = contact.getFixtureA().getBody(); Body bodyB = contact.getFixtureB().getBody(); //Hero death by crushing if(bodyA.getUserData().equals("Tom") || bodyB.getUserData().equals("Tom")){ for(int i = 0; i < impulse.getNormalImpulses().length; i++){ if(impulse.getNormalImpulses()[i] > GameConstants.CRUSH_IMPULSE){ System.out.println("Oh noes ! Major Tom has been crushed !!"); } } } }
About this code :
public static float CRUSH_IMPULSE = 300;
The value of 300 is completely arbitrary. I'll probably change it when the time of fine tuning comes.
And here is the result ! Simple, isn’t it ?
Notice : The code I put in the postSolve will also detect any collision that produces an impulse that exceeds the threshold, for example if the hero flies at very high speed and hit a wall. I could make a condition that the hero dies only if he is crushed by a piston, but I like the idea that he could die by a high-speed collision, that would add challenge to the game. So for now, I stick with that code.