Movatterモバイル変換


[0]ホーム

URL:


Skip to main content

itch.ioitch.io logo & titleitch.io logo

Browse GamesGame JamsUpload GameDeveloper LogsCommunity
Log inRegister
Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

Apprentice Soft

57
Posts
2
Topics
25
Followers
8
Following
A member registered Dec 13, 2015 ·View creator page →
@ApprenticeSoft

Creator of

This is a prototype of 3rd person farming/land managing game.
A Bubble Shooter game with minimalist design!
This game is a remake of the classic Jezz Ball, with a minimalist design.
Minimal Brick Breaker is a brick breaker game with minimalist design.
Escape your wrecked spaceship to survive!
Adventure
A simple physics based platformer with a tricky gameplay
Platformer
Escape your wrecked space ship to survive !
Adventure

Recent community posts

Hi there!

Indeed, the dev retired from indie-deving in 2019 and none of his projects will be updated.
Thanks for the fix to make the game run on 64-bit platforms! This game was developped when 32-bit was still predominant.

Hi, this is a great initiative.

I would like to add my own modest contribution by adding Cosmonaut to the bundle.

https://apprenticesoft.itch.io/cosmonaut

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

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

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 gamehere.

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

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


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, withAudacity... 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 onFreesound.org.


Implementing the sounds

Implementing sounds in libGDX isvery easy. There are 2 class :Sound andMusic.

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 theAssetManager, in theLoadingScreen.java :

//Loading of the soundsgame.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 theGameScreen.java.

backgroundSound = game.assets.get("Sounds/Background.wav", Music.class);backgroundSound.setLooping(true);backgroundSound.play();backgroundSound.setVolume(0.15f);

Note thesetLooping() 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)orsound.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 theLeak.java, I create the Sound in thecreator():

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 theactive() function in theLeak.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 thePiston.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 thecodes of the differents objects, for the variations.

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

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 theLevelSelectionScreen.java, we need this code in theshow() :

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

TheButtonAction.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. ItslevelListener()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 !

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 aTable, with manyTextButtons. I already talked about the of Table and TextButton to create theHUD.

The key part of theLevelSelectionScreen.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 afor 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 theGameConstants.NUMBER_OF_LEVEL, is a number I stored in theGameConstants.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 islocalToStageCoordinates. 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 arraylevels, 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 thecreator():

//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 positionstage.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 2TextButtonStyles, 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 thecreator() :

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 aLabel to display the "Chose a level" title to obtain the screen displayed in the gif, at the beginning of this post.

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 thePreferences.

Firs, let's create theData.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 theLoad() 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 thegetLevel() function and we can modify its value with thesetLevel() 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 thecreate() :

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 theObstacle.java, and I call the needed method depending on the need to use a NinePatch or a TextureRegion.

Here are thedraw() 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 theObstacleLight.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 theLeak.java.

For the gas leak, I had to create an animation, so I drew 10 sprites, and theLeak.java has its owndraw() 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 theObstacleDoor.java and theItemSwitch.java.


TheItemSwitch.java has its owndraw() 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 theObstacleRevolving.java.

Moving Obstacle

This sprite is used by theObstacleMoving.java.


Items : Fuel and oxygen refill

These sprites are use by theFuelRefill.java andOxygenRefill.java.

Level Exit Door

This sprites are used by theExit.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

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 thedisplacement() method of theMyCamera.java:

//Zoom-in/Zoom-outif (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 andviewportHeight instead of uzing the zoom field of thecamera ?

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 thatzoomLimit() 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 thiszoomlimit() 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 theTiledMapRenderer, I need to implement adraw() method in the entities I want to draw.

Basically, the way I built my code, I have theObstacle 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 someObstacle entities that will require aNinePatch (Walls and Pistons), and someObstacle entities that will require aTexture (all the other Obstacles I think).Texture andNinePatch don't use the samedraw 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 inArrays :

In theTiledMapReader.java, I store theObstacles that need aTexture in an array called "obstacles" and I store theObstacles that need aNinePatch in an array calledobstaclesWithNinePatch.

For that, I only need to do

obstacles.add(obstacle);

or

obstaclesWithNinePatch.add(obstacle);

Drawing the entities

Once the variousObstaclesstored in the rightArray, I can draw them in theGameScreen.java, by calling the rightdrawmethod, whether theObstacle uses aTexture or aNinePatch.

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

TheObstacle.java was reorganize to have a constructor that need a TextureAtlas as argument, which is needed for the use of NinePatch.

NowObstacle.java have theseconstructors :

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 byObstacleMoving.java
You can see that these constructors are empty. All the code of the Body creation has been moved in acreate() method.

And there are also twodraw() 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 theTiledMapReader, I had a for loop that checked what type ofObstacle need to be created, and by default, it created anObstacle. I did a little change, and created aWall class, and now, the for loop creates aWall by default.

Here is the code ofWall.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 theObstaclePiston.java, and I could spend a whole post on it. Just click the link to see the code in the repository.

Really an interesting post on the profiling !

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

Graphic update : Tiled Map

After setting up the scrolling star background, I worked on the map itself. For that I use the amazing level editorTiled.

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 someprecise rulesto 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 folderandroid --->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 theGameScreen.java.

First we need to create theTiledMap and theTiledMapRenderer in theconstructor :

tiledMap = new TmxMapLoader().load("Levels/Level 3.tmx");tiledMapRenderer = new OrthogonalTiledMapRendererWithSprites(tiledMap, GameConstants.MPP, game.batch);

Then in therenderer() 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);//Backgroundgame.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 maptiledMapRenderer.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 theObstacles, we can't do that withTiled. As far as I know,Tiled can't draw moving objects. Thus, I'll need to implement adraw() method for every type ofObstacle.
  • 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.

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 theGameScreen.java.

In thecreatorof theGameScreen.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 thecreator, 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 therender() of theGameScreen.java I draw theTexture with theright 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 verticalgame.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 theOffset argument of thedraw method, but I don't know how yet.

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 !

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 filesTom_Animation.png andTom_Animation.pack. I put them inandroid --->assets --->Images

Loading the animation spritesheet

In theLoadingScreen.java, I load the spritesheet, like I load anyTexture 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 2Animations, based on 1TextureAtlas. Easy, ain't it ?

Details of anAnimation 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 argumentis 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 adraw() function in theHero.java. Thatdraw() function will be called in the GameScreen.java, between abatch.begin() and abatch.draw().

Here is thedraw() function of theHero.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 thiscode :

  • The draw function takes 2 arguments :
    • ASpriteBatch, to draw the animation, it's always the sameSpriteBatch that use, the one I created in theMyGdxGame.java.
    • A float that I callANIM_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 theGameConstants.java, and update it in the render loop of theGameScreen.java with this line :GameConstants.ANIM_TIME += Gdx.graphics.getDeltaTime();
  • With the lineif(Gdx.input.isKeyPressed(Keys.W) && fuelLevel > 0), I check if the jetpack is activated. If yes, I play thetomFly animation, else I play the tomIdle animation.
  • I also added a boolean calledfly to theHero.java. This boolean allows us to check if the jetpack is on or off.
  • Theif(!fly) andif(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 drawfunction 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 !


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 softwareSpriter 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 themtutorial 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 !

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.

Creating the HUD (2/2)

Here are the functions of theHUD.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 correspondingTable. It will then increase the alpha of theImage and theTable so it becomes visible and the player can interact with it. Note that the functionswin(),lose() andpause()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 adraw() function that will be called in therender() loop of theGameScreen. This function will draw the oxygen level and fuel level bars.
  • Finally I created abuttonListener() that will be called in theshow() of theGameScreen . This function describes the action of each button.

In the GameScreen.java:

To use the HUD, we need to add it to theGameScreen.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 newrender():
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 theContactListener to displaywinTable andloseTable 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!

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 aHUD to display informations on theGameScreen.

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 theHUD.javacreator :

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 severalTables, TextButtons andLabels and anImage:
    • ATable is a widget in which we can put widgets and actors. It's very useful to organize things.
    • ALabel display a text. Note that to create aLabel, you need to define aLabelStyle.
    • AnImage is... an image, yeah that's right.
  • I create oneTable for each event : Win, Lose and Pause
  • In each Table I put aLabel that tells what's happening, and I put severalTextButtons, so the player can chose to play again or go to the main menu screen.
  • AButton can be added to only oneTable, 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 anImage "imageTableBackground" that will be used as theTables 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 theTables,Labels andImage 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 theStage. Note that I don't define anyStage in theHUD, because we'll use theGameScreen Stage.

Main Menu Screen

TheAsset Manager has been set up, and some fonts and sprites sheets were loaded during the loading screen. Now I can create aMainMenuScreen.

TheMainMenuScreen code is very simple. Basically, the screen is the same as theLoadingScreen, but it will also display a "Play" button :


Here is the code of theMainMenuScreen.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 theLoadingScreen.
  • 3 new entities are created : aStage, aTextButton and aSkin.
    • TheStage is an input processor. We can put all our actors (likeButtons) in theStage, and theStage will receive input events.
    • TheTextButton is an actor. It will trigger a determined event when clicked.
    • TheSkin stores resources to create our UI. In my case, I only store theTextureAtlas I loaded in theAsset Manager in theSkin.
  • To create aTextButton, we need to first define aTextButtonStyle. TheTextButtonStyle allows us to determine every parameter of theTextButtonlike its font, color, appearance when it is up, down, or checked... Once theTextButtonStyle created, creating aTextButton 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 theStage.
  • To make theMainMenuScreen 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 theStage, only to have the fade-in effect : stage.act();
  • And we draw theStage :stage.draw();

In the show():

  • To allow theStage 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 aClickListener.

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 theAsset 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 usingFreeTypeFonts. 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 !

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 aNinePatch and why do we needNinePatchs? 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. TheNinePatch 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 aNinePatch vs scaling a normal image.


Thus, I created my first assets, composed of 4NinePatchs, with the tooldraw9patch (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 theTextureAtlas

Now that I have my first assets, I need to pack them in a single png image with theTexture Packer that you can downloadhere. 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 anAsset Manager ! For theAsset Manager , I create it in the loading screen.

Creating the loading screen

Actually, we already have theAsset 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 theAsset 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 theTexture 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 folderAndroid --->Asset. In theAsset 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 theLoadingScreen.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 theTextureAtlas : it only require this code line :game.assets.load("Images/Images.pack", TextureAtlas.class); And when will need to access thisTexture Atlas from theAsset 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 useFreeTypeFonts. 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.ttffile of the font you want in yourAsset folder.
  • In therender(), we go to theMainMenuScreenonly once theAsset Manager finished loading everything we wanted to load (game.assets.update()).

And that's it ! We have our first assets : Pictures packed in aTexture Atlas and fonts. All these assets are loaded in theAsset 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 :


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)
  • ATiledMapReader.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 theOxygenRefill andFuelRefill classes, that are subclasses of anItem class.

Here is the code of theItem.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 :

  • Thecreate() function is quite basic : It reads theTiled Map to get coordinates of the item and create thebody.
  • Anitem possess a "used" boolean, that is set tofalse 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 therender() of theGameScreen. It checks if theItem has been used, if yes, the item is removed from the level.
Item.javapossesses 2 sublcasses :FuelRefill.java andOxygenRefill.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 theactivate()function run is adding these two lines in theGameConstants.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 itemsfor(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 theTiledMapReader.java :

public void active(){        hero.displacement();                for(Obstacle obstacle : obstacles)            obstacle.active();                for(Item item: items)            item.active(this);    }

This function will run in therender() of theGameScreen.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 thebeginContact function of theContactListener, in theGameScreen :

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 thisObstacleRevolving.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 extendsObstacle
  • ObstacleRevolvingis formed by a KinematicBody
  • I read the properties of theTiledMapObject to determine the rotation speed...
  • ... and I apply the angular velocity
  • Finally, theactivate() function called when we use anItemSwitch to control theObstacleRevolving sets the angular velocity to 0 or to the speed value, depending on if we turn theObstacleRevolvingon oroff.
And guess what code we put in the mainfor loop of theTiledMapReader.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 ofObstacleDoor.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 ofObstacle.
  • 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 booleanactive.
The following is straightforward, if you followed this devlog :

TheTiledMapReader.java need to recognize theObstacleDoor 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 !


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 anAssociation Number. For that, as usual, I set a property inTiled, 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 severalAssociation Numbers to the switch, by separating them with a comma, if I want the switch to control several objects.

Here is the code forItemSwitch.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 theTiledMapObject to see if the switch ison oroff by default.
  • Then I read the properties to obtain all theAssociation 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 theactive() function that will be called if there is a contact between Major Tom and the switch. Theactive() function will check for eveyAssociation Number stored in the String Array if one of the Obstacle in the map posses the sameAssociation Number. If yes, the function modifies the Obstacle'sactivate() function.

To use theItemSwitch, with theObstacles, I need to do some modifications in Obstacle.java:

  • Add an intassociationNumber
  • Add a booleanactive
  • Add a functionactivate()

Theactivate() function will be defined in each type of Obstacle. For now it the same function for bothObstaclePiston andObstacleMoving :

public void activate(){        active = !active;    }
And of course, theactive() function of every type of Obstacle must take into account the new boolean active (I guess it starts to be really confusing betweenactive,active() andactivate()) 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 ofLeak.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 haveaddBody() andremoveBody() functions that will be called in the GameScreen each time a body enters or exits the gas spray.
  • Finally, in theactive() 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.
TheTiledMapReader.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 mainfor 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 theGameScreen.java, we'll usebeginContact andenContact functions of theContactListener 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 ofObstacle : TheObstacleMoving.

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 anObstacleMoving... once you typed all the code, haha.


Modifying the Obstacle.java

First, obviously, thisObstacleMoving requires aPolylineMapObject instead of aRectangleMapObject, thus we can't use the Obstacle.java as is. We need to add a creator that takes into account thePolylineMapObject.

Here is the new creator inObstacle.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 aPolylineMapObject.

And here is the code of theObstacleMoving.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 thePolylineMapObject to determine the speed, the dimension and the behavior of theObstacleMoving.
  • Then we put the coordinate of every points in an Array, which will form the path that theObstacle will follow
  • Then we create the body of theObstacle.
  • And we give a direction and an impulse to initiate the motion.
  • Finally, it theactive() method, we check if the Obstacle is between 2 points of thepathArray, and we update the direction every time theObstacle is not between 2 points.
This ObstacleMoving gives us opportunities to create some cool design :

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 theContactListener we set in theGameScreen, 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 thepostSolve method of theContactListener in theGameScreen.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 theGameConstants.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 thepostSolve 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.

itch.io on Twitteritch.io on Facebook
AboutFAQBlogContact us
Copyright © 2025 itch corp ·Directory ·Terms ·Privacy ·Cookies

[8]ページ先頭

©2009-2025 Movatter.jp