🤑 Indie game store🙌 Free games😂 Fun games😨 Horror games
👷 Game development🎨 Assets📚 Comics
🎉 Sales🎁 Bundles

Apprentice Soft

53
Posts
2
Topics
1
Followers
8
Following
A member registered Dec 13, 2015 · View creator page →

Games

Recent community posts

Very cool game ! Great 3D, nice graphs and very challenging !

I think that's my favorite game for this jam. Very well done !

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.

Controls

  • A : Rotate counter-clockwise
  • D : Rotate clockwise
  • W : Use the jetpack
  • O : Camera zoom-out
  • P : Camera zoom-in
  • Esc : Pause
itch.io community » Game Development · Created a new topic Cosmonaut
(Edited 2 times)

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.

Thanks a lot !
Glad you enjoy my devlog, I did my best on this !

(Edited 2 times)

Game submission !

Finally ! I submitted my game !

After a huge rush:

  • I created 8 levels. I planned to create 10, but it took much more time than I thought.
  • I implemented lights with Box2DLights. It's pretty basic, but it add atmosphere... I guess... hahahah

Here is the last gameplay video :


And here is the link to play my game :

http://itch.io/jam/libgdxjam/rate/51123


Controls

  • A : Rotate counter-clockwise
  • D : Rotate clockwise
  • W : Use the jetpack
  • O : Camera zoom-out
  • P : Camera zoom-in
  • Esc : Pause

Gameplay

  • For each level, you have limited oxygen and limited jetpack fuel.
  • Oxygen decreases with time, fuel decreases each time you use the jetpack.
  • Use your jetpack wisely !
  • When you touch a wall, you can propel yourself by rotating, it's a way to move without using fuel. You have less control though.


Here is the source on GitHub


(Edited 1 time)

The home stretch !

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.

Sounds !

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 the sounds

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.

Kotlin seems interesting. Did you find it difficult to transition from Java to Kotlin ?

(Edited 1 time)

Level Selection Screen : Using the buttons !

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 !

(Edited 1 time)

Level Selection Screen

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.

Creation of the level selection table

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.

Creation of the "Back" button

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 :


Differentiating between completed levels and not complete levels

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.

Thanks for the tip ! ;)

Saving data

Before working on the sounds, I still have few things to do, in order to have a game fully playable :

  1. Saving data : For this game we'll only save the number of levels we completed, so we won't need to start the game from start every time we play.
  2. Create a level selection screen

Saving data

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()++);

Graphic update : Doors, switchs, gas leaks, moving obstacles, items

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 !

The code

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 ?

Light Obstacle

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.

Gas leak

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);
}

Doors and switch

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); 
        }
    }

Revolving obstacles

This sprite is used with the ObstacleRevolving.java.

Moving Obstacle

This sprite is used by the ObstacleMoving.java.


Items : Fuel and oxygen refill

These sprites are use by the FuelRefill.java and OxygenRefill.java.

Level Exit Door

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

(Edited 2 times)

Camera Zoom

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 !

Graphic update : Walls and Obstacles

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 :

Organizing the Entities

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);

Drawing the entities

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


Reorganization of the Obstacle.java

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);
}
  1. Constructor for Obstacle requiring a Texture
  2. Constructor for Obstacle requiring a NinePatch
  3. Constructor only used by ObstacleMoving.java
You can see that these constructors are empty. All the code of the Body creation has been moved in a create() method.

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);
}

Creation of a Wall class

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);
    }
}

Drawing a NinePatch

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.

Replied to bazola in bazola's DevLog

Really an interesting post on the profiling !

I didn't know about this tool VisualVM . I have to download it, it seems really useful !

(Edited 1 time)

Graphic update : Tiled Map

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.

  • For the Obstacles, we can't do that with Tiled. As far as I know, Tiled can't draw moving objects. Thus, I'll need to implement a draw() method for every type of Obstacle.
  • For the walls, it's different. Tiled is definitely made to draw walls. But I wanted to give me the option to draw thin walls. By thin, I mean walls with a width thinner than 100 px (which is a tile dimension in my project). And I want to be able to draw walls of any thickness. Therefore, the solution that came to my mind was using a NinePatch to draw walls. And I'll talk about that in the next devlog.
(Edited 2 times)

Graphic update : scrolling background

There remains only 12 days until the end of the libGDX Jam, and I still have A LOT of work to do, mainly on the graphics and the sounds. As the time pass, my code is less and less clean. I do things as they come and don't really take time to go back on my code to optimize or clean it. So this is it. I prefer to warn you, I'll present more and more dirty code ! hahaha.
Yesterday I posted a youtube video showing the graphic updates of Cosmonaut. Maybe we don't see it very well in the video, but through the different windows and holes, we can see stars scrolling in background.


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.

(Edited 2 times)

Graphic update

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 !

(Edited 1 time)

Creating the in-game animations

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

Loading the animation spritesheet

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);


Creating the animations

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 :

  1. 1st argument is the duration of a single frame. I put 0.1f, so every frame of my animations will last 0.1 second. Thus, an animation with 20 frames will last 2 seconds
  2. 2nd argument is the name of the frames (contained in the spritesheet) we'll use to create the animation. It's mandatory that all the frames of a single animation have the same name, and are differentiated with a number (ex : Tom_Idle_000, Tom_Idle_001...)
  3. 3rd argument is the animation mode. This one is pretty self-explanatory. You can play the animation with various modes like NORMAL, LOOP, REVERSED...


Using the animations

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 :

  • The draw function takes 2 arguments :
    • A SpriteBatch, to draw the animation, it's always the same SpriteBatch that use, the one I created in the MyGdxGame.java.
    • A float that I call ANIM_TIME. This float is used to know which frame of the animation to draw at a given time. For that, I need to add this float in the GameConstants.java, and update it in the render loop of the GameScreen.java with this line : GameConstants.ANIM_TIME += Gdx.graphics.getDeltaTime();
  • With the line if(Gdx.input.isKeyPressed(Keys.W) && fuelLevel > 0), I check if the jetpack is activated. If yes, I play the tomFly animation, else I play the tomIdle animation.
  • I also added a boolean called fly to the Hero.java. This boolean allows us to check if the jetpack is on or off.
  • The if(!fly) and if(fly) statement is very useful to check the transition between Idle and Fly animations : With these statements, we check if we begin of the 2 action that are "Fly" and "Stay Idle". If we begin a new action we MUST put the GameConstants.ANIM_TIME to zero. Thus we can take the animation from the beginning, it is to say, from the 1st frame.
  • Then, to draw my animations, I need to use the draw function that use 10 arguments. This function allows us to draw a sprite with a rotation. This is appropriated to my game, as Major Tom will rotate. Here are the 10 arguments :
    • The frame to draw
    • The X position of the frame
    • The Y position of the frame
    • The X position of the rotation center
    • The Y position of the rotation center
    • The width of the frame
    • The height of the frame
    • The X scale factor
    • The Y scale factor
    • The rotation angle


Dealing with the collision detection

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 !


(Edited 2 times)

Hero polishing and animation

Polishing

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.

Animation

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 :

  1. You draw every single frame. You can do anything you can imagine. The only limit is your drawing skill
  2. You draw once, every body parts, and you use a software to move them, as if your character was a puppet.

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 !

(Edited 5 times)

Drawing : Hero conception

And voilà ! I reach to the much dreaded part : Draw

  • The game mechanistic is pretty much set.
  • The game as a name, a logo, a loading screen and a main menu screen, a HUD.
  • I also have an asset manager.

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.

(Edited 3 times)

Creating the HUD (2/2)

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 :

  • I created several functions that will be called when the corresponding event happens : win(), lose(), outOfFuel(), pause(). Basically, these functions will set the size and position of the imageTableBackground based on the size of the corresponding Table. It will then increase the alpha of the Image and theTable so it becomes visible and the player can interact with it. Note that the functions win(), lose() and pause() use a new boolean called "GameConstants.GAME_PAUSED". This boolean is stored in the GameConstants.java even though it is not a constant because I was too lazy to create a class only for it ! We set it to true to make the game stop when the game ends or is on pause.
  • I created a draw() function that will be called in the render() loop of the GameScreen. This function will draw the oxygen level and fuel level bars.
  • Finally I created a buttonListener() that will be called in the show() of the GameScreen . This function describes the action of each button.

In the GameScreen.java :

To use the HUD, we need to add it to the GameScreen.java :

  • In the creator : Only add this line
hud = new HUD(game, stage, skin, mapReader.hero);
  • In the render() : The render loop needs more modification, here is the new render() :
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();
    }
  • In the show() : add this line
hud.buttonListener();

We also need to modify the ContactListener to display winTable and loseTable in the corresponding situation :

  • In the beginContact() :
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();  
}
  • And the new postSolve() :
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!

(Edited 2 times)

Creating the HUD (1/2)

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 :

  • I create several Tables, TextButtons and Labels and an Image:
    • A Table is a widget in which we can put widgets and actors. It's very useful to organize things.
    • A Label display a text. Note that to create a Label, you need to define a LabelStyle.
    • An Image is... an image, yeah that's right.
  • I create one Table for each event : Win, Lose and Pause
  • In each Table I put a Label that tells what's happening, and I put several TextButtons, so the player can chose to play again or go to the main menu screen.
  • A Button can be added to only one Table, thus, if I want the pauseTable, and loseTable to have a "Menu" button, I need to create two "Menu" button, one for the pauseTable and one for the loseTable.
  • I create an Image "imageTableBackground" that will be used as the Tables background.
  • I create a Label to tell to the play to press "ESC" to restart the level when he is out of fuel, as he won't be able to control the hero without fuel. But I want to let the player decide if he wants to wait until the hero is out of oxygen.
  • I set the Tables, Labels and Image alpha to 0 so they are invisible at the beginning of the game. Example of the talbeLose : tableLose.addAction(Actions.alpha(0));
  • To finish the creator, I add every actors to the Stage. Note that I don't define any Stage in the HUD, because we'll use the GameScreen Stage.
(Edited 1 time)

Main Menu Screen

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 :

  • I create the background image, with exactly the same code I used to display the logo image during the LoadingScreen.
  • 3 new entities are created : a Stage, a TextButton and a Skin.
    • The Stage is an input processor. We can put all our actors (like Buttons) in the Stage, and the Stage will receive input events.
    • The TextButton is an actor. It will trigger a determined event when clicked.
    • The Skin stores resources to create our UI. In my case, I only store the TextureAtlas I loaded in the Asset Manager in the Skin.
  • To create a TextButton, we need to first define a TextButtonStyle. The TextButtonStyle allows us to determine every parameter of the TextButton like its font, color, appearance when it is up, down, or checked... Once the TextButtonStyle created, creating a TextButton requires only one line : playButton = new TextButton("PLAY", textButtonStyle);
  • After creating all this, we only need to add the actors, it is to say, the image and the button to the Stage.
  • To make the MainMenuScreen fancier I make the "Play" button appears with a fade-in effect, with this line : playButton.addAction(Actions.sequence(Actions.alpha(0) ,Actions.fadeIn(0.25f)));

In the render() :

  • We need to animate the Stage, only to have the fade-in effect : stage.act();
  • And we draw the Stage : stage.draw();

In the show():

  • To allow the Stage to receive input, we need this line of code : Gdx.input.setInputProcessor(stage);
  • The we define the action triggered by the "Play" button. For that we set up a ClickListener.

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 !


Detailing the FreeTypeFont creation process

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 !

(Edited 2 times)

Creation of the 1st assets !

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 :

1. draw9patch.bat

2.Texture Packer.jar

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 :

  • In the creator, I first create the logo picture and the stage that will contain the picture.
  • I then load the texture atlas in the Asset Manager. See how easy it is to load the Texture Atlas : it only require this code line : game.assets.load("Images/Images.pack", TextureAtlas.class); And when will need to access this Texture Atlas from the Asset Manager, it will be as easy, with this single code line : game.assets.get("Images/Images.pack", TextureAtlas.class);
  • I then create fonts and load them in the Asset Manager. Note that for the fonts, I always use FreeTypeFonts. I find them VERY convenient to create fonts which the size will adapt to the screen size. For that, you first need to put a .ttf file of the font you want in your Asset folder.
  • In the render(), we go to the MainMenuScreen only once the Asset Manager finished loading everything we wanted to load (game.assets.update()).

And that's it ! We have our first assets : Pictures packed in a Texture Atlas and fonts. All these assets are loaded in the Asset Manager. Now we are ready to use them to create our UI.

A name and a logo !

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 :


(Edited 1 time)

What I have after one week of libGDX Jam

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.

What I have

  • A (basic) plot
  • A hero character that I can control (Hero.java)
  • A camera that follows the hero movements (MyCamera.java)
  • Light objects that float and can be pushed (ObstacleLight.java)
  • Objects that move along a predetermined path that was drawn in Tiled (ObstaclesMoving.java)
  • Pistons like objects that move back and forth (ObstaclePiston.java).
  • Objects that revolve around an axis (ObstacleRevolving.java).
  • Doors that can be open/closed with a switch (ObstacleDoor.java).
  • Switches that can open/close doors and also enable/disable objects (ItemSwitch.java)
  • Gas leak that pushes the hero or any floating object that crosses the gas spray (Leak.java)
  • Items that the hero can pick up to refill the oxygen or jetpack fuel (OxygenRefill.java and FuelRefill.java)
  • A TiledMapReader.java to read the level that I created and create all the above-mentioned objects.
  • A winning condition : The hero exits the room.
  • Two losing conditions :
    • The hero is out of oxygen
    • The hero is crushed by a moving object

What I have to do

  • Give a name to that project
  • The HUD
  • The main menu screen
  • The graphics
  • The sounds
  • An asset manager
  • A loading screen

That's still A LOT to do !

Merry Christmas everyone !

Items : Oxygen and Fuel Refill

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 :

  • The create() function is quite basic : It reads the Tiled Map to get coordinates of the item and create the body.
  • An item possess a "used" boolean, that is set to false by default
  • The activate() function will be called when Major Tom picks up the item. It will be defined in each subclass as the activity of the item will depend on its nature.
  • The active() function will run in the render() of the GameScreen. It checks if the Item has been used, if yes, the item is removed from the level.
Item.java possesses 2 sublcasses : FuelRefill.java and OxygenRefill.java.

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 !


One last animated obstacle : Revolving Obstacle !

OK, here is probably the last Obstacle I’ll create. After that, I think I have enough to design cool levels. Revolving Obstacles... revolves... yeah I know, kinda obvious.

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 :

  • ObstacleRevolving extends Obstacle
  • ObstacleRevolving is formed by a KinematicBody
  • I read the properties of the TiledMapObject to determine the rotation speed...
  • ... and I apply the angular velocity
  • Finally, the activate() function called when we use an ItemSwitch to control the ObstacleRevolving sets the angular velocity to 0 or to the speed value, depending on if we turn the ObstacleRevolving on or off.
And guess what code we put in the main for loop of the TiledMapReader.java ? Yeah, you’re right :
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 we have switches, let's create doors !

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 :

  • ObstacleDoor is a subclass of Obstacle.
  • First I read check in the Properties of the TiledMapObject for the opening speed value.
  • Then I set the closed and open positions of the door.
  • Finally, the active() function move the door to the open/closed position according to the value of the boolean active.
The following is straightforward, if you followed this devlog :

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 !


(Edited 3 times)

Interacting with the environment : Switches

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 :

  • First, I read the properties of the TiledMapObject to see if the switch is on or off by default.
  • Then I read the properties to obtain all the Association Numbers of the switch. For that I set up a String array.
  • Then I create the body of the switch, that is a sensor.
  • Finally, I create the active() function that will be called if there is a contact between Major Tom and the switch. The active() function will check for evey Association Number stored in the String Array if one of the Obstacle in the map posses the same Association Number. If yes, the function modifies the Obstacle's activate() function.

To use the ItemSwitch, with the Obstacles, I need to do some modifications in Obstacle.java:

  • Add an int associationNumber
  • Add a boolean active
  • Add a function activate()

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 :


Simulating a gas leak

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 :

  • Leak extends Obstacle
  • Leak is a sensor, so there are no physical collisions with a leak. But it still detects collisions.
  • Then we set up an HashSet called fixtures, where we’ll gather and manage all the fixtures that enter and exit gas spray.
  • We then read the properties of the rectangle we drew in Tiled to get the speed.
  • The following bunch of code lines automatically deduct the position of the leak origin and the direction of the gas spray.
  • Then we have addBody() and removeBody() functions that will be called in the GameScreen each time a body enters or exits the gas spray.
  • Finally, in the active() function we apply a force to all the bodies that are in the gas spray. Note that the force decreases as you are farther from the leak origin.
The TiledMapReader.java needs to recognize the leak. No surprises for that, it’s always the same thing, you only need to add few code lines in the main for loop :
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 !

Animated Obstacle : Moving Obstacle

Always with the view of having more level designing possibility and a richer gameplay, we'll create another kind of Obstacle : The ObstacleMoving.

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 :

  • First we check the properties of the PolylineMapObject to determine the speed, the dimension and the behavior of the ObstacleMoving.
  • Then we put the coordinate of every points in an Array, which will form the path that the Obstacle will follow
  • Then we create the body of the Obstacle.
  • And we give a direction and an impulse to initiate the motion.
  • Finally, it the active() method, we check if the Obstacle is between 2 points of the path Array, and we update the direction every time the Obstacle is not between 2 points.
This ObstacleMoving gives us opportunities to create some cool design :

(Edited 2 times)

Losing condition : Major Tom gets crushed !

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 :

  1. beginContact
  2. endContact
  3. preSolve
  4. postSolve

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 :

  • First we check if Major Tom is involved in the collision that has been detected
  • The we check if one of the impulses exceeds the max value we put in the GameConstants.java. One of the impulses ? Yes, when you have a collision between two bodies, each of the bodies undergoes an impulse, therefore, for one collision, two impulses.
  • If one of the impulse exceeds the threshold, we print a message in the console.
Don't foget to add this line in the GameConstants.java :
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.

(Edited 4 times)

Animated Obstacles : Piston (2/2)

Making the TiledMapReader recognize the ObstaclePiston

Here is the updated code for TiledMapReader .java :

private OrthographicCamera camera;
    private World world;
    private MapObjects objects;
    public Array<Obstacle> obstacles;
    private Array<MapObject> pistons;
    public Hero hero;
    
    public TiledMapReader(final MyGdxGame game, TiledMap tiledMap, World world, OrthographicCamera camera){
        this.camera = camera;
        this.world = world;
        
        hero = new Hero(world, camera, tiledMap);
        
        objects = tiledMap.getLayers().get("Objects").getObjects();


        obstacles = new Array<Obstacle>();    
        pistons = new Array<MapObject>();
        
        for (RectangleMapObject rectangleObject : objects.getByType(RectangleMapObject.class)) {
            if(rectangleObject.getProperties().get("Type") != null){
                //End of the level
                if(rectangleObject.getProperties().get("Type").equals("Exit")){
                    Exit finish = new Exit(world, camera, rectangleObject);
                    obstacles.add(finish);
                }
                //Light obstacles
                else if(rectangleObject.getProperties().get("Type").equals("Light")){
                    ObstacleLight obstacle = new ObstacleLight(world, camera, rectangleObject);
                    obstacles.add(obstacle);
                }
                //Pistons
                else if(rectangleObject.getProperties().get("Type").equals("Piston")){
                    pistons.add(rectangleObject);
                }
            }
            else{
                Obstacle obstacle = new Obstacle(world, camera, rectangleObject);
                obstacles.add(obstacle);
            }
        }
        
        //Pistons creation
        for(int i = pistons.size - 1; i > -1; i--){
            if(pistons.get(i).getProperties().get("Group") != null){
                for(int j = 0; j < pistons.size; j++){
                    if(Integer.parseInt(pistons.get(i).getProperties().get("Group").toString()) == Integer.parseInt(pistons.get(j).getProperties().get("Group").toString()) &&
                            i != j){                  
                        ObstaclePiston piston = new ObstaclePiston(world, camera, pistons.get(i), pistons.get(j));
                        obstacles.add(piston);
                        
                        pistons.removeIndex(i);
                        pistons.removeIndex(j);
                        i--;
                    }
                }
            }    
            else
                System.out.println("Piston creation failed");
        }
    }
Differences with the previous TiledMapReader.java :
  • First, we create an Array : pistons = new Array<MapObject>
  • In the for loop, we store every Rectangles which "Type" is Piston in the pistons Array.
  • Outside the main for loop, we create another for loop dedicated to create ObstaclePiston : It will check if 2 objects in the pistons Array have the same "Group" number, create an ObstaclePiston from these 2 objects before removing them from the pistons Array and adding the newly create ObstaclePiston to the obstacles Array.

Finally, we need to run the active() method in the GameScreen

In the render() of the GameScree.java, we only need to add this lines :

for(Obstacle obstacle : mapReader.obstacles){
            obstacle.active();
}

Here is a gif of the result :

Now that we have these ObstaclePiston, we need to create the losing condition "Hero gets crushed" !

Animated Obstacles : Piston (1/2)

Next type of Obstacle : Piston

With this Obstacle, we increase the complexity in our code : The piston needs 2 fixtures (Head and Axis), and it needs to move. This will give us the opportunity to create traps for the hero, and also the opportunity to have another losing condition : Hero gets crushed.

Here is a short video showing the process of creating the Pistons with Tiled and the result after running the code :

Creating the Piston in Tiled

In Tiled, for each Piston, we need to draw 2 Fixtures, one for the Head and one for the Axis of the Piston. The TiledMapReader will need to know that the 2 Rectangles we drew are connected to form a Piston, otherwise, the TiledMapReader will convert these 2 Rectangles in 2 simple Obstacles, it is to say, 2 walls.

To show that these 2 Rectangles form a Piston, will simply add a property called "Group" to these 2 Rectangles, and we’ll attribute the same value to, say 1, to the property "Group" of the 2 Rectangles. Of course, if we create several Pistons, for example 4 Pistons, we will have Group 1, Group 2, Group 3, and Group 4.

Now let’s see the code

Here is the code for ObstaclePiston.java :

public class ObstaclePiston extends Obstacle{

    private PolygonShape shape2;
    private float width2, height2, posX2, posY2;
    private float speed = 10;
    private float delay = 0;
    private Vector2 initialPosition, finalPosition, direction;
    private Vector2[] travel;
    private int step = 1;
    
    public ObstaclePiston(World world, OrthographicCamera camera, MapObject rectangleObject1, MapObject rectangleObject2) {
        super(world, camera, rectangleObject1);
        
        //Delay before activation
        if(rectangleObject1.getProperties().get("Delay") != null){
            delay = Float.parseFloat((String) rectangleObject1.getProperties().get("Delay"));
        }
        else if(rectangleObject2.getProperties().get("Delay") != null){
            delay = Float.parseFloat((String) rectangleObject2.getProperties().get("Delay"));
        }
        
        //Motion speed
        if(rectangleObject1.getProperties().get("Speed") != null){
            speed = Float.parseFloat((String) rectangleObject1.getProperties().get("Speed"));
        }
        else if(rectangleObject2.getProperties().get("Speed") != null){
            speed = Float.parseFloat((String) rectangleObject2.getProperties().get("Speed"));
        }
        
        //Creation of the second Fixture
        Rectangle rectangle2 = ((RectangleMapObject) rectangleObject2).getRectangle();
        
        width2 = (rectangle2.width/2) * GameConstants.MPP;
        height2 = (rectangle2.height/2) * GameConstants.MPP;
        posX2 = (rectangle2.x + rectangle2.width/2) * GameConstants.MPP;
        posY2 = (rectangle2.y + rectangle2.height/2) * GameConstants.MPP;
        
        shape2 = new PolygonShape();
        shape2.setAsBox(width2, height2, new Vector2(posX2 - posX, posY2 - posY), 0);
        
        bodyDef.position.set(new Vector2((rectangle2.x + rectangle2.width/2) * GameConstants.MPP, (rectangle2.y + rectangle2.height/2) * GameConstants.MPP));
        
        fixtureDef = new FixtureDef();
        fixtureDef.shape = shape2;
        fixtureDef.density = 0;  
        fixtureDef.friction = 0.5f;  
        fixtureDef.restitution = 0.5f;
        
        body.createFixture(fixtureDef);  
        body.setUserData("ObstaclePiston");        
        shape2.dispose();

        if(rectangleObject1.getProperties().get("Part").equals("Head")){
            body.getFixtureList().get(0).setUserData("ObstaclePiston");
            body.getFixtureList().get(1).setUserData("Obstacle");
            
            //initialPosition = body.getPosition();
            initialPosition = new Vector2(posX, posY);
            if(posX == posX2)
                finalPosition = new Vector2(initialPosition.x, initialPosition.y + rectangle2.height * Math.signum(posY2 - posY) * GameConstants.MPP);
            else
                finalPosition = new Vector2(initialPosition.x + rectangle2.width * Math.signum(posX2 - posX) * GameConstants.MPP, initialPosition.y);
        }
        else {
            body.getFixtureList().get(0).setUserData("Obstacle");
            body.getFixtureList().get(1).setUserData("ObstaclePiston");

            //initialPosition = body.getPosition();
            initialPosition = new Vector2(posX, posY);
            if(posX == posX2)
                finalPosition = new Vector2(initialPosition.x, initialPosition.y + rectangle.height * Math.signum(posY - posY2) * GameConstants.MPP);
            else
                finalPosition = new Vector2(initialPosition.x + rectangle.width * Math.signum(posX - posX2) * GameConstants.MPP, initialPosition.y);
        }

        travel = new Vector2[2];
        travel[0] = initialPosition;
        travel[1] = finalPosition;

        direction = new Vector2();
        direction = new Vector2(travel[step].x - body.getPosition().x, travel[step].y - body.getPosition().y);
    }
    
    @Override
    public BodyType getBodyType(){
        return BodyType.KinematicBody;
    }

    @Override
    public void active(){
        if(delay > 0){
            delay -= Gdx.graphics.getDeltaTime();
        }
        else{
            if(!new Vector2(travel[step].x - body.getPosition().x, travel[step].y - body.getPosition().y).hasSameDirection(direction)){            
                if(step > 0)
                    step = 0;
                else step = 1;
                
                direction = new Vector2(travel[step].x - body.getPosition().x, travel[step].y - body.getPosition().y);
            }
            body.setLinearVelocity(direction.clamp(speed, speed)); 
        }
    }
}

About this code :

  • ObstaclePiston is a subclass of Obstacle
  • After super(world, camera, rectangleObject1); , you can see a bunch of code lines to read the different properties of the piston to determine if there is a delay before the ObstaclePiston starts moving and the speed of the motion.
  • Then we create the second Fixture. Notice that the second Fixture is included in the Body (body.createFixture(fixtureDef);), thus there is only one Body.
  • Then there is another bunch of code lines that I am too lazy to detail. It allows to determine which Fixture is the Head and which Fixture is the Axis, and deduce from their positions what will be the direction of the ObstaclePiston motion.
  • Finally, the active() method applies the eventual delay before starting the motion and update the motion direction each time the ObstaclePiston reaches the end of its stroke.