Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

Apprentice Soft's devlog

A topic by Apprentice Soft created Dec 19, 2015 Views: 12,061 Replies: 46
Viewing posts 1 to 20 of 43 · Next page · Last page
Submitted (3 edits)

Hey,

So finally, I create my devlog. I spent a part of the evening brainstorming, I read the different develogs and saw some already interesting ideas of RTS and RPG. On my side, I'll go on a much simpler and less ambitious project. I needed an idea that I can realize alone, with my limited coding skills and non-existent skills in drawing and designing sounds, haha.

So I came up with this idea of puzzle-platformer game in which you control a guy, in a jetpack equipped space suit that tries to escape his wrecked spaceship.



EDIT

Submitted (2 edits)

Plot

- Ground control : Ground control to major Tom.

- Major Tom : hu ?

- Ground control : We've got bad news.

- Major Tom : hu ?

- Ground control : Well… you know… hum… that intern I told you about last week ?

- Major Tom : hu ?

- Ground control : Yeah, that's him. Well, he fucked up. He was assigned to the asteroid detection while you cross the asteroid belt, and… how to say ? He was doing a good job the 1st month, and I didn't watch him that much lately, and this little brat, while he was supposed to monitor the approaching asteroids, spent his time playing to Candy Crush on Facebook and…

- Major Tom : HE TOOK ADVANTAGE OF MY ABSENCE TO BEAT MY SCORE !?!?!?!?

- Ground control : No… hum… long story short : You are going right into a very dense region of the asteroid belt. And given your speed, there is no way you can avoid it. Actually you can reach this region at any moment.

- Major Tom : hu ?

- Ground control : Yeah, if you are lucky enough to don't hit a huge asteroid you'll still have to face a micrometeoroid rain that will shatter your spaceship.

- Major Tom : So… what's the plan?

- Ground control : You'll have a painful death.

- Major Tom : hu?

- Ground control : One last thing: We just blew a 72 thousand billion dollars mission, and to amortize the costs we had this idea of making a movie about your story. We thought you had your word on this. We approached Ridley Scott, Christopher Nolan and Woody Allen to direct the movie, do you have any preferences?

MICROMETEOROID RAIN HITS THE SPACESHIP

- Computer : Warning! Warning ! Oxygen leak detected. Artificial gravity system damaged.

- Major Tom : Dammit! Where is my space suit?



Concept

The game will involve Box2D. The player will control Major Tom in his jetpack equipped space suit. The aim is to reach the safety spaceship. Each level will be a room of the main spaceship.

Here is an ugly concept screen :




Gameplay

  • Control with WAD
    • W : linear impulse with the jetpack
    • A and D : counter-clockwise and clockwise rotation
  • No gravity
  • No air friction
  • Fuel limitation : The longer you press W the more you consume fuel. Use you jetpack wisely to make it to the room exit
  • Air limitation : You have to exit the room before you run out of air.

Basically, if you use too much your jetpack to go fast, you'll end up without fuel, but if you go really slowly to save fuel, you won't have enough air. There might be air and fuel supply to pick up in hardest levels.

Submitted (1 edit)

Time to write the devlog !

I've been thinking a lot about the form that my devlog would take. I asked myself why I wanted to join this jam. Two things came up : Having fun, and sharing experiences. Therefore, I decided to make a full tutorial, with code and everything. Starting from the very basic, and yet boring for anyone taking part to this jam : Installing and setting up a libGDX project. The jam is one month long, so I guess I can afford to spend 50% of the time I dedicate to this jam for the purpose of writing a decent tutorial devlog.

For the record :

- I am absolutely not a pro, thus, my code won't be clean, and I'll present the things the way I do them which is not necessarily the best way to do them.

- I work with Eclipse IDE, even though since summer 2015 Android Studio is the official IDE for Android, and everywhere you'll see that you must migrate to Android Studio as soon as possible. But I am lazy, and as long as my projects created with Eclipse work, I won't migrate. Thus my tutorials will only refer to Eclipse.


So here we go ! Let's start by the very beginning !

Those who already know libGDX may directly jump to the next post.


Setting up the libGDX project

Setting up the environment

To use libGDX with Eclipse, you'll need to set your work environment up. For that you'll need to install

To install ADT and Gradle, you can simply use the Eclipse Marketplace : Once you installed Eclipse, go in Help ---> Eclipse Marketplace. In the marketplace, just do a research on ADT and Gradle.

Once you finished setting up your work environment, you can set up your project !

Setting up the project

Just go on the libGDX download page and install the latest version of libGDX and start it, you'll get this screen :


On this screen you'll chose the name of your project, package, game class, the folder where you'll create your project, and the android SDK location. If you don't remember where you installed it, start Eclipse, go in Window ---> Preferences ---> Android and at the top of the Preferences window you'll find the SDK location link . Just copy and paste the link in the libGDX window.

Still in the libGDX window, you can choose which platform you want your game to run on, and you can install some extensions. I know for sure I'll need Box2d and Freetype, and I install Tools and Box2dlights "just in case". Finally you can generate the project.

Importing the project

Now that you created your libGDX project, you need to import it in Eclipse. For that, in Eclipse go in File ---> Import.

You'll get this screen :


Click on Gradle Project and Next, and you'll get this screen :


On this screen browse to the location of your project and find the folder core. Then click on Build Model, check all the part of the project you want to import to Eclipse (android, desktop....) knowing that you absolutely need core, and also desktop as we'll run the game on desktop during all the coding.


And that's it ! Your libGDX is set up, and you are ready to develop your game !

The default project

If you open the core folder, you'll there is only one class, MyGdxGame. Soon, we'll create a lot of other class and packages to make a lot of cool stuff !


To run this default project, right-click on the folder desktop and go to Run As ---> Java Application ---> DesktopLauncher.

You'll get this screen displaying the Bad Logic logo :



This is it for the basic setup !

As from the next post we attack the real fun ! Game coding !

Submitted (1 edit)

Time to code !

Basic setup

First of all, we'll modify the DesktopLauncher.java to choose a resolution and a name for the game window. As I still don't have a name for my project, I'll simply go with "Life in Space", and I chose a resolution of 960 * 640 for the development.

Here is my DesktopLauncher.java :

public class DesktopLauncher {
    public static void main (String[] arg) {
        LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
        config.title = "Life in Space";
        config.width = 960;
        config.height = 640;
        new LwjglApplication(new MyGdxGame(), config);
    }
}

Then I modify the MyGdxGame.java by extending Game.java, so I can use screens.

Here is the MyGdxGame.java :

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

You can see that I created a SpriteBatch, that I'll use to draw all the graphics of the game, and an AssetManager, that will be useful later to load the assets, when I'll have assets. Then you can see that in the create() I call this.setScreen(new GameScreen(this));, thus the game display the game screen. Later I'll have more screens, like a LoadingScreen that I'll display when I load the assets, and a MainMenuScreen.

Important : In the render(), don't forget to do super.render(); !

And here is what the freshly created GameScreen class look like.

GameScreen.java :

public class LoadingScreen implements Screen{
    final MyGdxGame game;
    
    public LoadingScreen(final MyGdxGame game){
        this.game = game;
    }

    @Override
    public void render(float delta) {
        // TODO Auto-generated method stub      
    }

}

Now let's run this project !


This class displays absolutely nothing for now !

What am I waiting to make this class displaying an amazing game ?

Well... it's not that simple. My project will involve physics, thus I'll use Box2D, and to create the levels, I'll use Tiled. Therefore, before having fun with creating level and game mechanics, I need to integrate Box2D and Tiled to the project.

Integrating Box2D and Tiled to the project

Tiled

Tiled is trully an amazing level editor. Not only you draw beautiful maps using your tileset, but you can also draw shapes, like rectangles, ellipses, polygones, lines that Box2D can convert into bodies to integrate in the game.

Here is an illustration of the logic :


So where do we start ?

I'll go in the order of the logic exposed above, thus I'll start by drawing a quick map with Tiled, and then we'll see code. A LOT.

Here is the map, called Level 1 :

In this screenshot, in the right column, at the top, I created 3 layers : Objects, Spawn and Tile Layer 1.

  • Objects : This is an object layer. In object layers you can draw polygons and put images. In this layer I draw the polygons that will be converted to bodies by Box2D.
  • Spawn : This is also an object layer. In this layer I put an image from my tileset, that will represent the hero spawn points. For now my tileset only contains this red dot as I didn't draw anything yet. If I click on the red dot, I can give it a name (right column top) I call it Tom. That'll be important later.
We are done with Tiled for now. Now let's create bodies with Box2D !
Submitted (2 edits)

Box2D (1/2)

OK, we now have a level created ! Now it's time to read this map, and create the bodies !

First, we'll create a GameConstants class, that will contain a lot of constants that we need to have access during the game. That we'll make things easier to handle.

Here is the GameConstants.java :

public class GameConstants {
    //World constants
    public static float MPP = 0.05f;            //Meter/Pixel
    public static float PPM = 1/MPP;            //Pixel/Meter
    public static float BOX_STEP = 1/60f; 
    public static int BOX_VELOCITY_ITERATIONS = 6;
    public static int BOX_POSITION_ITERATIONS = 2;
    public static float GRAVITY = 0;
    public static float DENSITY = 1.0f;
    
    //Tiled Map constants
    public static int PPT = 32;                    //Pixel/Tile
    
    //Screen constants
    public static int NB_HORIZONTAL_TILE = 50;
    public static float SCREEN_WIDTH = MPP * NB_HORIZONTAL_TILE * PPT;
    public static float SCREEN_RATIO = (float)Gdx.graphics.getHeight()/(float)Gdx.graphics.getWidth();
    public static float SCREEN_HEIGHT = SCREEN_WIDTH * SCREEN_RATIO;
    
    //Hero constants
    public static float HERO_HEIGHT = 1.5f * PPT * MPP / 2;
    public static float HERO_WIDTH = HERO_HEIGHT / 2;
    public static float JETPACK_IMPULSE = 100;
    public static float TOM_ROTATION = 5;
}
  • There are World constants that are relative to Box2D. MPP and PPM are very important : By default in Box2D, 1 pixel = 1 m. Thus, the object you create by default are HUGE. And the Box2D simulation doesn't work for huge sizes and huge mass. Therefore the MPP and PPM factor conversions will be used to scale down the world and to sprites. For now I chose a conversion of 20 pixels for 1 meter.
  • There is also the Tiled Map Constant that is simply the size of one tile, 32 pixels in my case. That will be important to set up the camera.
  • The screen constants, that are actually camera constants, are there simply to chose the size of the viewport. For example, I chose I wanted my camera to display 50 tiles on the width. The number of tiles that will be displayed on the height will depend on the screen ratio.
  • An then I created the hero constants : His size, the power of his jetpack and the rotation speed.

All these are only starting values. As the development goes by, I will certainly modify these values according to the gameplay I want.

Now that we have these constants, we need a TiledMapReader to read the map we created and created bodies :

Here is the TiledMapReader.java :

public class TiledMapReader {

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

        obstacles = new Array<Obstacle>();    
        
        for (RectangleMapObject rectangleObject : objects.getByType(RectangleMapObject.class)) {
            Obstacle obstacle = new Obstacle(world, camera, rectangleObject);
            obstacles.add(obstacle);
        }
    }
}

You can see, this calsse reads reads the tiledMap and create the hero and the objects. The TiledMapReader class read the layer, and find the layer called "Objects" in which I created the walls, and returns the object found in this layer. Then, for each object in this layer, if the object is a rectangle, we create an Obstacle and we put it in an Obstacle Array.

Here is the class Hero.java :

public class Hero {

    public Body heroBody;
    private BodyDef bodyDef;
    private FixtureDef fixtureDef;
    private PolygonShape heroShape;
    private float width, height, posXInit, posYInit;
    private Vector2 direction;
    
    public Hero(World world, Camera camera, TiledMap tiledMap){

        MapObjects personnages = (MapObjects)tiledMap.getLayers().get("Spawn").getObjects();

        width = GameConstants.HERO_WIDTH;
        height = GameConstants.HERO_HEIGHT;
        posXInit = (personnages.get("Tom").getProperties().get("x", float.class) + personnages.get("Tom").getProperties().get("width", float.class)/2) * GameConstants.MPP;
        posYInit = (personnages.get("Tom").getProperties().get("y", float.class) + personnages.get("Tom").getProperties().get("height", float.class)/2) * GameConstants.MPP;
        direction = new Vector2();
        
        heroShape = new PolygonShape();
        heroShape.setAsBox(width, height);
        
        bodyDef = new BodyDef();
        bodyDef.position.set(posXInit, posYInit);
        bodyDef.type = BodyType.DynamicBody; 
        
        heroBody = world.createBody(bodyDef);
        heroBody.setFixedRotation(false);
            
        fixtureDef = new FixtureDef();  
        fixtureDef.shape = heroShape; 
        fixtureDef.density = (float)(GameConstants.DENSITY/(width * height));
        fixtureDef.friction = 0.01f;  
        fixtureDef.restitution = 0.1f;  
        heroBody.createFixture(fixtureDef).setUserData("Tom"); 
        heroBody.setUserData("Tom");
        
        heroShape.dispose();
    }
    
    public void displacement(){    
        if(Gdx.input.isKeyPressed(Keys.W)){
            heroBody.applyForceToCenter(new Vector2(0, GameConstants.JETPACK_IMPULSE).rotate(heroBody.getAngle() * MathUtils.radiansToDegrees), true);
        }
        
        if(Gdx.input.isKeyPressed(Keys.A))
            heroBody.setAngularVelocity(GameConstants.TOM_ROTATION);
        else if(Gdx.input.isKeyPressed(Keys.D))
            heroBody.setAngularVelocity(- GameConstants.TOM_ROTATION);
        else
            heroBody.setAngularVelocity(0);
    }
    
    public float getX(){
        return heroBody.getPosition().x;
    }
    
    public float getY(){
        return heroBody.getPosition().y;
    }
    
    public Vector2 getOrigine(){
        return new Vector2(posXInit, posYInit);
    }
}

The Hero class reads the tiledMap, find the layer called "Spawn" and look for an object called "Tom". Then it get the coordinate of Tom and creates a body at this coordinates. This class contains a displacement() method to control the hero with WAD keys. There are also few others method that could be useful during the development.

Submitted (2 edits)

Box2D (2/2)

Here is the Obstacle.java :

public class Obstacle {
    
    public Body body;
    protected BodyDef bodyDef;
    protected FixtureDef fixtureDef;
    protected PolygonShape polygonShape;
    public float posX, posY, width, height, angle;
    Camera camera;
    
    public Obstacle(World world, Camera camera, MapObject rectangleObject){        
        create(world, camera, rectangleObject);     
    }
    
    public void create(World world, Camera camera, MapObject rectangleObject){

        Rectangle rectangle = ((RectangleMapObject) rectangleObject).getRectangle();
            
        this.camera = camera;
        this.posX = (rectangle.x + rectangle.width/2) * GameConstants.MPP;
        this.posY = (rectangle.y + rectangle.height/2) * GameConstants.MPP;
        this.width = (rectangle.width/2) * GameConstants.MPP;
        this.height = (rectangle.height/2) * GameConstants.MPP;
        
        if(rectangleObject.getProperties().get("rotation") != null)
            this.angle = -Float.parseFloat(rectangleObject.getProperties().get("rotation").toString())*MathUtils.degreesToRadians;
        
        polygonShape = new PolygonShape();
        polygonShape.setAsBox(width, height);


        bodyDef = new BodyDef();
        bodyDef.position.set(new Vector2(posX, posY));
        bodyDef.type = getBodyType();
        body = world.createBody(bodyDef);
        
        fixtureDef = new FixtureDef();
        fixtureDef.shape = polygonShape;
        fixtureDef.density = (float)(GameConstants.DENSITY/(width * height));  
        fixtureDef.friction = 0.5f;  
        fixtureDef.restitution = 0.5f;
   
        body.createFixture(fixtureDef).setUserData("Obstacle");
        body.setUserData("Obstacle");
        
        if(rectangleObject.getProperties().get("rotation") != null){
            /*
             * To obtain x' et y' positions from x et y positions after a rotation of an angle A
             * around the origine (0, 0) :
             * x' = x*cos(A) - y*sin(A)
             * y' = x*sin(A) + y*cos(A)
             */
            float X = (float)(body.getPosition().x - width + width * Math.cos(angle) + height * Math.sin(angle));
            float Y = (float)(width * Math.sin(angle) + body.getPosition().y + height - height * Math.cos(angle));
            body.setTransform(X, Y, this.angle);
        }
        
        polygonShape.dispose();  
    }
    
    public float getWidth(){
        return width;
    }
    
    public float getHeight(){
        return height;
    }
    
    public float getX(){
        return posX;
    }
    
    public float getY(){
        return posY;
    }

    public void setX( float X){
        posX = X;
    }

    public void setY( float Y){
        posY = Y;
    }

    public void active(){     
    }

    public BodyType getBodyType(){
        return BodyType.StaticBody;
    }
}

The Obstacle classe takes a RectangleMapObject given by the TiledMapReader and create a body using the coordinates and the size of the RectangleMapObject. The Obstacle classe as been thought to be a extended to create other obstacles, with different properties (moving obstacle, light or heavy obstacles...).

You can see in my code something that ight look not usual : The density of the obstacle is expressed like this :

fixtureDef.density = (float)(GameConstants.DENSITY/(width * height)); The reason is that it will allow me to easily control the mass of every object later.

OK, now we converted the Tiled Map into bodies with Box2D, now it's the time to have fun !!

Submitted

libGDX

Finally, let's modify the GameScreen !

Here is the GameScreen.java :

public GameScreen(final MyGdxGame game){
        this.game= game;

        camera = new OrthographicCamera();
        camera.setToOrtho(false, GameConstants.SCREEN_WIDTH, GameConstants.SCREEN_HEIGHT);
        camera.update();  
        
        world = new World(new Vector2(0, GameConstants.GRAVITY), true);
        World.setVelocityThreshold(0.0f);
        debugRenderer = new Box2DDebugRenderer();
        
        tiledMap = new TmxMapLoader().load("Levels/Level 1.tmx");
        tiledMapRenderer = new OrthogonalTiledMapRendererWithSprites(tiledMap, GameConstants.MPP, game.batch);

        mapReader = new TiledMapReader(game, tiledMap, world, camera); 
    }

    @Override
    public void render(float delta) {  
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        camera.update();
        
        world.step(GameConstants.BOX_STEP, GameConstants.BOX_VELOCITY_ITERATIONS, GameConstants.BOX_POSITION_ITERATIONS); 
        debugRenderer.render(world, camera.combined);    

        mapReader.hero.displacement();
    }
}
  • In the constructor I create the camera, with the size I define in the GameConstants.java, then I create the World. The line World.setVelocityThreshold(0.0f); allows object to move very slowly. This is not mandatory, but I like it that way. Then I create a debugRenderer, because I still have no graphics, so the debugRenderer is the only way to visualize our work. Finally, I create, the tiledMap, the tiledMapRenderer and the TiledMapReader.

Notice that the tiledMapRenderer is an OrthogonalTiledMapRendererWithSprites that I modified to take into account the Box2D conversion factor. The code will follow next.

  • In the render(), we need to clear the screen and update the camera, then run the Box2D simulation. The we run the debugRenderer to display the Box2D bodies, and finally we run the displacement() method to control the hero with the keyboard.

Here is the result when we run the game ! Pretty fun hu ?


Here is the OrthogonalTiledMapRendererWithSprites.java :

public class OrthogonalTiledMapRendererWithSprites  extends OrthogonalTiledMapRenderer{
    float unitScale = 1;
    
    public OrthogonalTiledMapRendererWithSprites(TiledMap map, Batch batch) {
        super(map, batch);
    }

    //Constructor that takes into account the Box2D scale (MPP) to render the sprites at the same size as the BOx2D bodies
    public OrthogonalTiledMapRendererWithSprites (TiledMap map, float unitScale, Batch batch) {
        super(map, unitScale, batch);
        
        this.unitScale = unitScale;
    }

    //Proceduraly draw the sprites we'll add to an object layer in our tiled map
    @Override
    public void renderObject(MapObject object) {
        if(object instanceof TextureMapObject) {
            TextureMapObject textureObj = (TextureMapObject) object;
            
            batch.draw(textureObj.getTextureRegion(), 
                        textureObj.getX(), 
                        textureObj.getY(), 
                        textureObj.getTextureRegion().getRegionWidth() * unitScale, 
                        textureObj.getTextureRegion().getRegionHeight() * unitScale);
        }
    }
}

That's all for today !

Deleted 8 years ago
Submitted

Thanks ! I'll do my best, for both the game and the devlog !

Submitted (1 edit)

Setting up the camera behavior

On the first gif I posted, at the end of the previous post, you can see that the camera doesn't move. It doesn't follow the hero, and at the end of the animation, the hero leaves the screen. We need to make the camera follow the hero. For exemple, we could center the camera on the hero. Thus the hero woul always appears exactly at the center of the screen. But that's not always the most eye pleasing choice. What I prefer is to determine a zone, at the center of the screen. If the hero is in this zone, the camera doesn't move, if the hero leaves this zone, the camera follows the hero. Plus, I don't want the camera to go outside of the map limit. Therefore, if the hero is in a corner, for example bottom left corner of the map, the camera won't go over the map limit in order to respect the condition "The hero must stay in the define zone". In this case, the hero will appear at the bottom left corner of the camera.

Here is an illustration :

For that, in the GameScreen.java, I'll replace the OrthographicCamera by a camera that I'll call MyCamera, that extends OrthographicCamera.

Here is the code for MyCamera.java :

public class MyCamera extends  OrthographicCamera{
    
    float posX, posY;
    
    public MyCamera(){
        super();
    }
 
    public void displacement(Hero hero, TiledMap tiledMap){
        //Positioning relative to the hero
        if(this.position.x < hero.getX() - Gdx.graphics.getWidth() * GameConstants.MPP/10)
            posX = hero.getX() - Gdx.graphics.getWidth() * GameConstants.MPP/10;
        else if(this.position.x > hero.getX() + Gdx.graphics.getWidth() * GameConstants.MPP/10)
            posX = hero.getX() + Gdx.graphics.getWidth() * GameConstants.MPP/10;
        if(this.position.y < hero.getY() - Gdx.graphics.getHeight() * GameConstants.MPP/10)
            posY = hero.getY() - Gdx.graphics.getHeight() * GameConstants.MPP/10;
        else if(this.position.y > hero.getY() + Gdx.graphics.getHeight() * GameConstants.MPP/10)
            posY = hero.getY() + Gdx.graphics.getHeight() * GameConstants.MPP/10;
        
        //Camera smooth motion
        this.position.interpolate(new Vector3(posX,posY,0), 0.45f, Interpolation.fade);
        
        //Positioning relative to the level map limits
        if(this.position.x + this.viewportWidth/2 > ((float)(tiledMap.getProperties().get("width", Integer.class)*GameConstants.PPT))*GameConstants.MPP)
            this.position.set(((float)(tiledMap.getProperties().get("width", Integer.class)*GameConstants.PPT))*GameConstants.MPP - this.viewportWidth/2, this.position.y, 0);
        else if(this.position.x - this.viewportWidth/2 < 0)
            this.position.set(this.viewportWidth/2, this.position.y, 0);
        if(this.position.y + this.viewportHeight/2 > ((float)(tiledMap.getProperties().get("height", Integer.class)*GameConstants.PPT))*GameConstants.MPP)
            this.position.set(this.position.x, ((float)(tiledMap.getProperties().get("height", Integer.class)*GameConstants.PPT))*GameConstants.MPP - this.viewportHeight/2, 0);
        else if(this.position.y - this.viewportHeight/2 < 0)
            this.position.set(this.position.x, this.viewportHeight/2, 0);    
    }
}

What is important in MyCamera, is the displacement() method :

  1. I check if the camera position (this.position.x and this.position.y) is inside a rectangle centered on the hero, of width 20% of the screen width (Gdx.graphics.getWidth() * GameConstants.MPP/10) and of height 20% of the screen height. If the camera is not in this rectangle, I update variables posX and posY in order to make the hero appear exactly at the border of this rectangle, not in the center, otherwise the camera would have a jerky motion.
  2. I interpolate the position of the camera, so transition is smooth. You can modify the speed of this interpolation by the second parameter (that is 0.45f in my case). The higher this parameter, the quicker the camera motion.
  3. Finally, I check if after moving the camera, the camera went over the map limit. If yes, I move back the camera in order to exactly fit the border of the map with the border of the screen.

Here is the result :

Notice that in my exemple, the map is 50 tiles wide, and the I setted up the camera in order to display 50 tiles on the width. Thus, the camera doesn't move on the side, in order to don't go over the map border.

Submitted (1 edit)

Work on the hero

Losing condition : Oxygen and fuel levels

In my game, my hero's spaceship is wrecked : The oxygen leaked and the artificial gravity module (Yes that exists in my game !) is out of order. The hero must use is jetpack equipped spacesuit to travel across his spaceship and reach the safety spaceship. While traveling, the hero must take care 2 parameters :

  1. Oxygen level : The oxygen level decrease with the time. When it reaches 0, the hero dies.
  2. Fuel level : The fuel level decrease every time the hero activate his jetpack to create an impulse. When the fuel level reaches 0, the hero can no longer move.

First, we'll add some constants to the GameConstants.java :

public class GameConstants {<span class="redactor-invisible-space">
<span class="redactor-invisible-space">    ...
</span></span>
    //Hero constants
    public static float HERO_HEIGHT = 1.5f * PPT * MPP / 2;
    public static float HERO_WIDTH = HERO_HEIGHT / 2;
    public static float JETPACK_IMPULSE = 100;
    public static float TOM_ROTATION = 5;
    public static float MAX_OXYGEN = 120;
    public static float MAX_FUEL = 100;
    public static float FUEL_CONSUMPTION = 5;

    ...
}

You can see that in the Hero constants block I added 3 constants :

  1. MAX_OXYGEN : basically, it's the number of second you can last when the oxygen level is at 100%
  2. MAX_FUEL : The maximum amount of fuel
  3. FUEL_CONSUMPTION : The amount of fuel you burn for a 1 second impulse.

Then, we need to modify the Hero.java (I only show the difference with the previous code) :

public class Hero {

    ...  
    private float oxygenLevel, fuelLevel;
    
    public Hero(World world, Camera camera, TiledMap tiledMap){

        ...
        oxygenLevel = GameConstants.MAX_OXYGEN;
        fuelLevel = GameConstants.MAX_FUEL;
        ....
    }
    
    public void displacement(){    
        oxygenLevel -= Gdx.graphics.getDeltaTime();
        
        if(Gdx.input.isKeyPressed(Keys.W)){
            heroBody.applyForceToCenter(new Vector2(0, GameConstants.JETPACK_IMPULSE).rotate(heroBody.getAngle() * MathUtils.radiansToDegrees), true);
            fuelLevel -= Gdx.graphics.getDeltaTime() * GameConstants.FUEL_CONSUMPTION;
        }
        
        if(Gdx.input.isKeyPressed(Keys.A))
            heroBody.setAngularVelocity(GameConstants.TOM_ROTATION);
        else if(Gdx.input.isKeyPressed(Keys.D))
            heroBody.setAngularVelocity(- GameConstants.TOM_ROTATION);
        else
            heroBody.setAngularVelocity(0);
    
        System.out.println("FuelLevel = " + fuelLevel + " || OxygenLevel = " + oxygenLevel);
    }

    public float getOxygenLevel(){
        return oxygenLevel;
    }
    
    public float getFuelLevel(){
        return fuelLevel;
    }
} 

You can see that I created the 2 float oxygenLevel and fuelLevel, and I initiate them with their Max values in the creator. Then, in the displacement(), I added

oxygenLevel -= Gdx.graphics.getDeltaTime();

which makes the oxygenLevel decrease by one unit every second. I think I should rename the displacement() method, since it not only deal with the hero displacement now...

I also modify the action when we press the W key :

if(Gdx.input.isKeyPressed(Keys.W)){ 
    heroBody.applyForceToCenter(new Vector2(0, GameConstants.JETPACK_IMPULSE).rotate(heroBody.getAngle() * MathUtils.radiansToDegrees), true); 
    fuelLevel -= Gdx.graphics.getDeltaTime() * GameConstants.FUEL_CONSUMPTION; 
}

Now when we press the W key, the fuelLevel decreases by GameConstants.FUEL_CONSUMPTION units every second.

In order to monitor the oxygenLevel and the fuelLevel in the console, I also added :

System.out.println("FuelLevel = " + fuelLevel + " || OxygenLevel = " + oxygenLevel);

And here is an illustration of the oxygen and fuel consumption :

You can see in the console that the FuelLevel doesn't decrease when no impulse is applied to the hero, while the OxygenLevel decreases all along the animation.


Finally, to be able to lose, in the render() of the GameScreen we need to add some code to check the oxygenLevel and fuelLevel, and if one of these falls under 0, the player loses.

For now, we'll only print a message in the console, when the player loses. In the render() of the GameScreen I added :

if(mapReader.hero.getOxygenLevel() < 0 || mapReader.hero.getFuelLevel() < 0)
            System.out.println("You lose !");

Edit : I decided to change the losing condition : Now we lose only if the hero is out of oxygen. It makes more sense. Even if the hero is out of fuel for his jetpack, he still can finish the level if he reaches the exit fo the room. For example, you have just enough of fuel to create one last impulse to direct your hero towards the exit door. Once the jetpack is out of fuel, without air friction and gravity, the hero will keep going in the same direction. If he reaches the exit door before being out of oxygen, he succeeds in passing the level.

Thus, in the render() of the GameScreen I have now :

if(mapReader.hero.getOxygenLevel() < 0) 
    System.out.println("You lose !");

In the future I'll have to create a pop-up box that asks the player if he wants to restart the level when he is out of fuel, in order to prevent the player from waiting to be out of oxygen when he has no more fuel and no hope to reach the exit.

Submitted

Winning condition : Exiting the room

To finish a level, you simply need to reach the exit of the room you are in. To detect the exit of the room, I created a body, and when the hero collides with this body, the room is exited !

First, an Exit class, that is a subclass from myObstacle class is created.

Here is the Exit.java :

public class Exit extends Obstacle{
    
    public Exit(World world, Camera camera, MapObject rectangleObject) {
        super(world, camera, rectangleObject);
        
        body.getFixtureList().get(0).setSensor(true);
        body.getFixtureList().get(0).setUserData("Exit");
        body.setUserData("Exit");
    }
}

The difference between an Exit and an Obstacle are :

  • The Exit is a sensor (body.getFixtureList().get(0).setSensor(true);). That means that the Exit body will detect collisions but there won’t be any physical response to this collision : The hero can pass throug the Exit.
  • The user data of the Exit’s body and fixture are set to "Exit" instead of "Obstacle" (.setUserData("Exit")): This will allow us to make the difference between the collision with an Obstacle and the Exit.
Once the Exit class created, we need to add an Exit to our level
For that we use the Tiled editor and create a rectangle. Then, we'll use one of the most useful feature of Tiled : We'll create a property in our rectangle and we'll call it "Type" and we'll give the value "Exit" :

Then the TiledMapReader needs to recognize the Exit

Here is the new TiledMapReader.java :

public class TiledMapReader {

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

        obstacles = new Array<Obstacle>();    
        
        for (RectangleMapObject rectangleObject : objects.getByType(RectangleMapObject.class)) {
            if(rectangleObject.getProperties().get("Type") != null){
                //End of the level
                if(rectangleObject.getProperties().get("Type").equals("Exit")){
                    Exit finish = new Exit(world, camera, rectangleObject);
                    obstacles.add(finish);
                }
            }
            else{
                Obstacle obstacle = new Obstacle(world, camera, rectangleObject);
                obstacles.add(obstacle);
            }
        }
    }
}

Modifications with the previous TiledMapReader.java :

  • In the for loop we add an if, that will check if the rectangle has a property called "Type". To access to the properties of a rectangle object, you need to do : rectangleObject.getProperties()
  • If the rectangle has a property called "Type", we check if the "Type" property as the value "Exit"
  • If the value of Type is Exit, we create an Exit, and add it to the obstacles Array
  • If the rectangle doesn’t have a property called "Type", we create an Obstacle.
Finally, in the GameScreen, we set a contactListener detecting the collision between bodies
The contactListener will be in the show() of the GameScreen.java :
@Override
    public void show() {
        world.setContactListener(new ContactListener(){
            @Override
            public void beginContact(Contact contact) {
                Body bodyA = contact.getFixtureA().getBody();
                Body bodyB = contact.getFixtureB().getBody();
                
                if(bodyA.getUserData() != null && bodyB.getUserData() != null) {
                    //Finish the level
                    if(bodyA.getUserData().equals("Tom") && bodyB.getUserData().equals("Exit"))
                        System.out.println("LEVEL FINISHED !!!!");
                    else if(bodyB.getUserData().equals("Tom") && bodyA.getUserData().equals("Exit"))
                        System.out.println("LEVEL FINISHED !!!!");
                }
            }

            @Override
            public void endContact(Contact contact) {
            }

            @Override
            public void preSolve(Contact contact, Manifold oldManifold) {
            }

            @Override
            public void postSolve(Contact contact, ContactImpulse impulse) {               
            }
        });
    }

The contactListener includes 4 methods :

  1. beginContact : Called when two fixtures start collide. You can trigger events when two bodies collide.
  2. endContact : Called when two fixtures stop being in contact. Same thing, you can trigger whatever you want at this moment.
  3. preSolve : Called before the contact is processed, it means before the simulation of this collision runs, before the beginContact. Very useful to ignore some contacts.
  4. postSolve : Called after the contact is processed.
In our case we use the beginContact to detect collisions between the Hero and the Exit. For that, we check if the UserData of the fixtures that collide are "Exit" and "Tom". If yes, we print "LEVEL FINISHED !!!!" in the console.

That's it ! So far, so good. We have a fully playable game :
  • A start point
  • A finish point
  • We can control the hero
  • We have winning and losing conditions
Now, we need to add fun to this !
Submitted

Adding some fun : Light Obstacles

The next step in the development of this game is adding several types of obstacles to enhance the gameplay. I’ll start by adding light obstacles, that hero can push.

For that, let’s start by creating the ObstacleLight, that is obviously a subclass of Obstacle.

Here is the ObstacleLight.java :

public class ObstacleLight extends Obstacle{

    public ObstacleLight(World world, Camera camera, MapObject rectangleObject) {
        super(world, camera, rectangleObject);
        
        body.setUserData("ObstacleLight");
        body.getFixtureList().get(0).setUserData("ObstacleLight");
        
        //Weight
        if(rectangleObject.getProperties().get("Weight") != null){
            body.getFixtureList().get(0).setDensity(
                    body.getFixtureList().get(0).getDensity() * Float.parseFloat(rectangleObject.getProperties().get("Weight").toString())
            );
            body.resetMassData();
        }
    }
    
    @Override
    public BodyType getBodyType(){
        return BodyType.DynamicBody;
    }
}

Differences with the Obstacle class :

  • The UserData of the body and fixture ane "ObstacleLight" instead of "Obstacle"
  • The BodyType is a DynamicBody, which means that the body will move under the action of forces (collision, gravity...)
  • The ObstacleLight can have various weights, depending on the value we give to the property "Weight" in the Tiled map editor. If we don’t put a property "Weight", the ObstacleLight will have a default weight, which is the same weight as the hero.

Talking about the weight, and the density, it’s time to explain the way I deal with this :
In Box2D, to create a Body, you first need to create a Fixture. The Body will be then created from this fixture, and some other properties you put in the BodyDef. Each Fixture will have a weight. But you can’t set the weight in Box2D. Instead you set the density of the Fixture with
fixtureDef.density
and the weight is calculated like this : weight = density * area of the shape.

The shape is also included in the Fixture by

fixtureDef.shape = shape

For that you use a Shape you created earlier, that can be a PolygonShape or a CircleShape.

Thus, for example, for a rectangle, the weight will be : weight = density * width * height.

The bigger, the heavier. Which is logical.

But, for my game I want a way to control precisely the weight of every Obstacle in my game, without having to do some calculation. Therefore, in my game, EVERY bodies will have the exact same weight by default. For that, I create a constant in the GameConstants, for the density :

public static float DENSITY = 1.0f;

And the default density of the fixture of every object is defined like this (Example of a rectangle box):

fixtureDef.density = (float)(GameConstants.DENSITY/(width * height));

I divide GameConstants.Density by the area of the fixture : The bigger, the least dense, and the weight is stable.


Why the hell do I need to work like that ?

Because it’s very easy to manage. For example, when I put obstacles in my level editor, and I want my hero to interact with the object, I don’t need to calculate what density I need to give to each obstacle, according to the difficulty I want the hero having to push these objects. With my system, every object will have the same weight as the hero by default. Thus I know that the hero can push them with a moderate effort, meaning, a moderate fuel consumption. If I want the hero to have trouble pushing an object I only need to add a property "Weight", and attribute it a high value, say 10, and I’ll know that this object will weigh 10 times more than the hero. If I want a really light object, I’ll attribute a low value to "Weight", say 0.1f, and I’ll know that the hero can displace it with no effort. And I can do that independently of the size of the object, which gives a great liberty for designing levels.


Then we need to modify the TiledMapReader to recognize the ObstacleLight

Thus, the for loop of the TiledMapReader.java becomes :

for (RectangleMapObject rectangleObject : objects.getByType(RectangleMapObject.class)) {
            if(rectangleObject.getProperties().get("Type") != null){
                //End of the level
                if(rectangleObject.getProperties().get("Type").equals("Exit")){
                    Exit finish = new Exit(world, camera, rectangleObject);
                    obstacles.add(finish);
                }
                //Light obstacles
                else if(rectangleObject.getProperties().get("Type").equals("Light")){
                    ObstacleLight obstacle = new ObstacleLight(world, camera, rectangleObject);
                    obstacles.add(obstacle);
                }
            }
            else{
                Obstacle obstacle = new Obstacle(world, camera, rectangleObject);
                obstacles.add(obstacle);
            }
        }

All that remains is to add ObstacleLight to the level in Tiled

For that, you only need to create rectangles and give a property "Type" with the value "Light". The created ObstacleLight will have the same weight as the hero by default, but just add a property "Weight" with any value to modify their weight.

And here is the result !


Submitted

Nice, this looks like a lot of fun!

Submitted

Thanks !

Submitted

Animated Obstacles : Piston (1/2)

Next type of Obstacle : Piston

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

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

Creating the Piston in Tiled

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

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

Now let’s see the code

Here is the code for ObstaclePiston.java :

public class ObstaclePiston extends Obstacle{

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

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

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

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

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

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

About this code :

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

Animated Obstacles : Piston (2/2)

Making the TiledMapReader recognize the ObstaclePiston

Here is the updated code for TiledMapReader .java :

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


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

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

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

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

Here is a gif of the result :

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

Submitted (2 edits)

Losing condition : Major Tom gets crushed !

OK now we have these nice pistons, Major Tom can get crushed by them, making the player lose the game.

Detecting the event "Tom gets crushed" happens in the ContactListener we set in the GameScreen, to detect collisions between bodies.

Remember, the ContactListener has 4 methods :

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

The one that interests us here is the postSolve method. The postSolve method gives us access to the impulse that a body undergoes after a collision. The idea is that the more the hero is crushed, the higher the impulse he undergoes. Thus, if the impulse exceeds a predetermined value, the hero dies.

Here is the code of the postSolve method of the ContactListener in the GameScreen.java :

public void postSolve(Contact contact, ContactImpulse impulse) {
                Body bodyA = contact.getFixtureA().getBody();
                Body bodyB = contact.getFixtureB().getBody();
                
                //Hero death by crushing
                if(bodyA.getUserData().equals("Tom") || bodyB.getUserData().equals("Tom")){ 
                    for(int i = 0; i < impulse.getNormalImpulses().length; i++){
                        if(impulse.getNormalImpulses()[i] > GameConstants.CRUSH_IMPULSE){
                            System.out.println("Oh noes ! Major Tom has been crushed !!");
                        }
                    }
                }
            }

About this code :

  • First we check if Major Tom is involved in the collision that has been detected
  • The we check if one of the impulses exceeds the max value we put in the GameConstants.java. One of the impulses ? Yes, when you have a collision between two bodies, each of the bodies undergoes an impulse, therefore, for one collision, two impulses.
  • If one of the impulse exceeds the threshold, we print a message in the console.
Don't foget to add this line in the GameConstants.java :
public static float CRUSH_IMPULSE = 300;

The value of 300 is completely arbitrary. I'll probably change it when the time of fine tuning comes.


And here is the result ! Simple, isn’t it ?


Notice : The code I put in the postSolve will also detect any collision that produces an impulse that exceeds the threshold, for example if the hero flies at very high speed and hit a wall. I could make a condition that the hero dies only if he is crushed by a piston, but I like the idea that he could die by a high-speed collision, that would add challenge to the game. So for now, I stick with that code.

Submitted

Animated Obstacle : Moving Obstacle

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

The ObstacleMoving is an obstacle that will follow a given path. You only have to draw the path in Tiled, and the code will generate an Obstacle that will follow this path, back and forth, or in a loop according to the properties you give it.

To draw the path, we'll use the polyline tool of Tiled :


And here is an animation showing how easy it is to create an ObstacleMoving... once you typed all the code, haha.


Modifying the Obstacle.java

First, obviously, this ObstacleMoving requires a PolylineMapObject instead of a RectangleMapObject, thus we can't use the Obstacle.java as is. We need to add a creator that takes into account the PolylineMapObject.

Here is the new creator in Obstacle.java :

public Obstacle(World world, OrthographicCamera camera, PolylineMapObject polylineObject){
        
    }

Yes, this creator is empty. It's only here in order to be able to create a subclass, ObstacleMoving.java, that uses a PolylineMapObject.

And here is the code of the ObstacleMoving.java :

public class ObstacleMoving extends Obstacle{

    private float speed;
    private boolean backward, loop;
    private Vector2 direction;
    private Vector2[] path;
    private int step;
    
    public ObstacleMoving(World world, OrthographicCamera camera, PolylineMapObject polylineObject) {
        super(world, camera, polylineObject);
        
        //SPEED
        if(polylineObject.getProperties().get("Speed") != null)
            speed = Float.parseFloat((String) polylineObject.getProperties().get("Speed"));
        else speed = 5;
        
        //DOES THE PATH MAKE A LOOP ?
        if(polylineObject.getProperties().get("Loop") != null)
            loop = true;
        else loop = false;
        
        //WIDTH OF THE MOVING OBJECT
        if(polylineObject.getProperties().get("Width") != null)
            width = Integer.parseInt((String) polylineObject.getProperties().get("Width")) * GameConstants.PPT * GameConstants.MPP/2;
        else
            width = 2 * GameConstants.PPT * GameConstants.MPP/2;
        
        //HEIGHT OF THE MOVING OBJECT
        if(polylineObject.getProperties().get("Height") != null)
            height = Integer.parseInt((String) polylineObject.getProperties().get("Height")) * GameConstants.PPT * GameConstants.MPP/2;
        else
            height = 2 * GameConstants.PPT * GameConstants.MPP/2;
        
        path = new Vector2[polylineObject.getPolyline().getTransformedVertices().length/2];
        for(int i = 0; i < path.length; i++){
            path[i] = new Vector2(polylineObject.getPolyline().getTransformedVertices()[i*2]*GameConstants.MPP, polylineObject.getPolyline().getTransformedVertices()[i*2 + 1]*GameConstants.MPP);
        }   
        
        polygonShape = new PolygonShape();
        polygonShape.setAsBox(width, height);

        bodyDef = new BodyDef();
        bodyDef.type = getBodyType();
        bodyDef.position.set(path[0]);
        
        fixtureDef = new FixtureDef();
        fixtureDef.shape = polygonShape;
        fixtureDef.density = 0.0f;  
        fixtureDef.friction = 0.0f;  
        fixtureDef.restitution = 0f;

        body = world.createBody(bodyDef);
        body.createFixture(fixtureDef).setUserData("Objet");
        body.setUserData("Objet");
        
        polygonShape.dispose();

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

    @Override
    public void active(){
        if(!loop){
            if(!backward){
                if(!new Vector2(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y).hasSameDirection(direction)){
                    step++;
                    
                    if(step == path.length){
                        backward = true;
                        step = path.length - 2;
                    }
                    
                    direction.set(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y);
                }
            }
            else{
                if(!new Vector2(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y).hasSameDirection(direction)){
                    step--;
                    
                    if(step < 0){
                        backward = false;
                        step = 1;
                    }
                    
                    direction.set(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y);
                }
            }    
        }
        else{
            if(!new Vector2(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y).hasSameDirection(direction)){
                step++;
                
                if(step == path.length){
                    step = 0;
                }
                
                direction.set(path[step].x - body.getPosition().x, path[step].y - body.getPosition().y);
            }
        }
        body.setLinearVelocity(direction.clamp(speed, speed)); 
    }
}

About this code :

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

Submitted

Simulating a gas leak

Here is the situation : Major Tom’s spaceship is completely wrecked because it entered a very dense region of the asteroid belt, thus it was hit by uncountable micrometeoroids. In some places, tubes carrying oxygen a various gases were punctured, causing gas leak.

In the absence of gravity, these gas leaks will propel anything that crosses the gas spray.

Here is an animation showing what happens when Major Tom pushes floating boxes in a gas leak :


Let's see the code of Leak.java :

public class Leak extends Obstacle{

    private Set<Fixture> fixtures;
    private Vector2 leakForce, leakOrigin;
    private float force, leakSize;
    
    public Leak(World world, OrthographicCamera camera,    MapObject rectangleObject) {
        super(world, camera, rectangleObject);
        
        body.getFixtureList().get(0).setSensor(true);
        body.getFixtureList().get(0).setUserData("Leak");
        body.setUserData("Leak");
        
        fixtures = new HashSet<Fixture>();
        
        //Leak force
        if(rectangleObject.getProperties().get("Force") != null){
            force = Float.parseFloat(rectangleObject.getProperties().get("Force").toString()) * GameConstants.DEFAULT_LEAK_FORCE;
        }
        else
            force = GameConstants.DEFAULT_LEAK_FORCE;
        
        //Leak direction and leak origine
        if(rectangle.width > rectangle.height){
            leakForce = new Vector2(force, 0);
            leakSize = rectangle.width * GameConstants.MPP;
            
            if(force > 0)
                leakOrigin = new Vector2(posX - width, posY);
            else
                leakOrigin = new Vector2(posX + width, posY);
        }
        else{
            leakForce = new Vector2(0, force);
            leakSize = rectangle.height * GameConstants.MPP;
            
            if(force > 0)
                leakOrigin = new Vector2(posX, posY - height);
            else
                leakOrigin = new Vector2(posX, posY + height);
        }
    }
    
    public void addBody(Fixture fixture) {
        PolygonShape polygon = (PolygonShape) fixture.getShape();
        if (polygon.getVertexCount() > 2) 
            fixtures.add(fixture);
    }

    public void removeBody(Fixture fixture) {
        fixtures.remove(fixture);
    }
    
    public void active(){
        for(Fixture fixture : fixtures){
            float distanceX = Math.abs(fixture.getBody().getPosition().x - leakOrigin.x);
            float distanceY = Math.abs(fixture.getBody().getPosition().y - leakOrigin.y);
            
            fixture.getBody().applyForceToCenter(    
                                                    leakForce.x * Math.abs(leakSize - distanceX)/leakSize, 
                                                    leakForce.y * Math.abs(leakSize - distanceY)/leakSize,
                                                    true
                                                );
        }
    }
}

About this code :

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

                //Leaks
                else if(rectangleObject.getProperties().get("Type").equals("Leak")){
                    Leak leak = new Leak(world, camera, rectangleObject);
                    obstacles.add(leak);
                }

                ...
            }
}

In the GameScreen.java, we'll use beginContact and enContact functions of the ContactListener to add or remove bodies from the gas spray :

public void beginContact(Contact contact) {
                Fixture fixtureA = contact.getFixtureA();
                Fixture fixtureB = contact.getFixtureB();
                
                if(fixtureA.getUserData() != null && fixtureB.getUserData() != null) {
                    //Leak
                    if (fixtureA.getUserData().equals("Leak") && fixtureB.getBody().getType() == BodyType.DynamicBody) {
                        for(Obstacle obstacle : mapReader.obstacles){
                            if(obstacle.body.getFixtureList().get(0) == fixtureA){
                                Leak leak = (Leak) obstacle;
                                leak.addBody(fixtureB);
                            }
                        }
                    } 
                    else if (fixtureB.getUserData().equals("Leak") && fixtureA.getBody().getType() == BodyType.DynamicBody) {
                        for(Obstacle obstacle : mapReader.obstacles){
                            if(obstacle.body.getFixtureList().get(0) == fixtureB){
                                Leak leak = (Leak) obstacle;
                                leak.addBody(fixtureA);
                            }
                        }
                    }                 
                }          
            }


            @Override
            public void endContact(Contact contact) {
                Fixture fixtureA = contact.getFixtureA();
                Fixture fixtureB = contact.getFixtureB();
                
                if(fixtureA.getUserData() != null && fixtureB.getUserData() != null) {
                    //Leak
                    if (fixtureA.getUserData().equals("Leak") && fixtureB.getBody().getType() == BodyType.DynamicBody) {
                        for(Obstacle obstacle : mapReader.obstacles){
                            if(obstacle.body.getFixtureList().get(0) == fixtureA){
                                Leak leak = (Leak) obstacle;
                                leak.removeBody(fixtureB);
                            }
                        }
                    } 
                    else if (fixtureB.getUserData().equals("Leak") && fixtureA.getBody().getType() == BodyType.DynamicBody) {
                        for(Obstacle obstacle : mapReader.obstacles){
                            if(obstacle.body.getFixtureList().get(0) == fixtureB){
                                Leak leak = (Leak) obstacle;
                                leak.removeBody(fixtureA);
                            }
                        }
                    }
                }
            }

And that's it ! Another feature for the level design !

Submitted (3 edits)

Interacting with the environment : Switches

OK, thus far I have animated obstacles like ObstaclePiston and ObstacleMoving, and I plan to have few others. It would be very interesting if we could enable/disable these animated obstacles with switches. And it would be interesting if one switch could control several obstacles at a time, and also if an obstacle could be controlled by several switches. It will allow me to create some puzzles.

Here is a video showing how that works :


As you can see in the video, to associate a switch with one or several animated object, I use an Association Number. For that, as usual, I set a property in Tiled, I give it the name "Association Number", and I give the same number to the switch and the animated object that I want to control. I can give several Association Numbers to the switch, by separating them with a comma, if I want the switch to control several objects.

Here is the code for ItemSwitch.java :

public class ItemSwitch {

    public Body swtichBody;
    private BodyDef bodyDef;
    private FixtureDef fixtureDef;
    private PolygonShape switchShape;
    private float width, height;
    private boolean isOn;
    private String[] associationNumbers;
    
    public ItemSwitch(World world,  OrthographicCamera camera, MapObject mapObject){
        create(world, camera, mapObject);
    }
    
    public void create(World world,  OrthographicCamera camera, MapObject mapObject){      
        //Is the switch on ?
        if(mapObject.getProperties().get("On") != null){
            if(Integer.parseInt((String) mapObject.getProperties().get("On")) == 1)
                isOn = true;
            else 
                isOn = false;
        }
        else
            isOn = false;
        
        //Association Numbers
        if(mapObject.getProperties().get("Association Number") != null){
            associationNumbers = mapObject.getProperties().get("Association Number").toString().split(",");
        }
        
        width = mapObject.getProperties().get("width", float.class)/2 * GameConstants.MPP;
        height = mapObject.getProperties().get("height", float.class)/2 * GameConstants.MPP;
        
        bodyDef = new BodyDef();
        fixtureDef = new FixtureDef();
        
        bodyDef.type = BodyType.StaticBody;

        bodyDef.position.set((mapObject.getProperties().get("x", float.class) + mapObject.getProperties().get("width", float.class)/2) * GameConstants.MPP,
                            (mapObject.getProperties().get("y", float.class) + mapObject.getProperties().get("height", float.class)) * GameConstants.MPP);
        
        switchShape = new PolygonShape();
        switchShape.setAsBox(width, height);
        
        fixtureDef.shape = switchShape;
        fixtureDef.density = 0;  
        fixtureDef.friction = 0.2f;  
        fixtureDef.restitution = 0f;
        fixtureDef.isSensor = true;
        
        swtichBody = world.createBody(bodyDef);
        swtichBody.createFixture(fixtureDef).setUserData("Switch");
        swtichBody.setUserData("Switch");     
    }
    
    public void active(Array<Obstacle> obstacles){    
        isOn = !isOn;
        
        for(String number : associationNumbers){
            for(Obstacle obstacle : obstacles)
                if(obstacle.associationNumber == Integer.valueOf(number))
                    obstacle.activate();
        }
    }
}

About this code :

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

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

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

The activate() function will be defined in each type of Obstacle. For now it the same function for both ObstaclePiston and ObstacleMoving :

public void activate(){
        active = !active;
    }
And of course, the active() function of every type of Obstacle must take into account the new boolean active (I guess it starts to be really confusing between active, active() and activate()) So basicaly, the new active() of every Obstacle looks like this :
public void active(){
        if(active){
           //Do the regular stuff
        }
        else
            body.setLinearVelocity(0, 0);         
    }

Finally, we need to detect collision between Major Tom and ItemSwitch

public void beginContact(Contact contact) {
                Fixture fixtureA = contact.getFixtureA();
                Fixture fixtureB = contact.getFixtureB();
                
                if(fixtureA.getUserData() != null && fixtureB.getUserData() != null) {       
                    //Switch
                    if(fixtureA.getUserData().equals("Tom") && fixtureB.getUserData().equals("Switch")){
                        for(ItemSwitch itemSwitch : mapReader.switchs){
                            if(itemSwitch.swtichBody == fixtureB.getBody())
                                itemSwitch.active(mapReader.obstacles);
                        }
                    }
                    else if(fixtureB.getUserData().equals("Tom") && fixtureA.getUserData().equals("Switch")){
                        for(ItemSwitch itemSwitch : mapReader.switchs){
                            if(itemSwitch.swtichBody == fixtureA.getBody())
                                itemSwitch.active(mapReader.obstacles);
                        }
                    }
                }
            }

And that's it ! Here is the result :


Submitted

Now we have switches, let's create doors !

Now that I can control objects with switches, I can create doors, that can be open/closed with switches.

Here is the code of ObstacleDoor.java :

public class ObstacleDoor extends Obstacle{
    
    private float speed = 5;
    private Vector2 initialPosition, finalPosition;

    public ObstacleDoor(World world, OrthographicCamera camera,    MapObject rectangleObject) {
        super(world, camera, rectangleObject);
        
        //Motion speed
        if(rectangleObject.getProperties().get("Speed") != null){
            speed = Float.parseFloat((String) rectangleObject.getProperties().get("Speed"));
        }
        
        initialPosition = new Vector2(posX, posY);
        
        if(width > height)
            finalPosition = new Vector2(posX + Math.signum(speed) * 1.9f*width, posY);
        else
            finalPosition = new Vector2(posX, posY + Math.signum(speed) * 1.9f*height);
    }
    
    @Override
    public BodyType getBodyType(){
        return BodyType.KinematicBody;
    }
    
    @Override
    public void active(){
        if(active)
            body.setLinearVelocity(    Math.signum(speed) * (initialPosition.x - body.getPosition().x) * speed, 
                                    Math.signum(speed) * (initialPosition.y - body.getPosition().y) * speed
                                    );
        else
            body.setLinearVelocity(    Math.signum(speed) * (finalPosition.x - body.getPosition().x) * speed,
                                    Math.signum(speed) * (finalPosition.y - body.getPosition().y) * speed
                                    );
    }

    @Override
    public void activate(){
        active = !active;
    }
}

About this code :

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

The TiledMapReader.java need to recognize the ObstacleDoor in the map :

for (RectangleMapObject rectangleObject : objects.getByType(RectangleMapObject.class)) {
            if(rectangleObject.getProperties().get("Type") != null){
                ...

                //Doors
                else if(rectangleObject.getProperties().get("Type").equals("Door")){
                    ObstacleDoor obstacle = new ObstacleDoor(world, camera, rectangleObject);
                    obstacles.add(obstacle);
                }

                ...
            }
        }

And here is the result !


Submitted

One last animated obstacle : Revolving Obstacle !

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

So here is the (short and easy) code for this ObstacleRevolving.java :

public class ObstacleRevolving extends Obstacle{
    
    private float speed = 90;

    public ObstacleRevolving(World world, OrthographicCamera camera, MapObject rectangleObject) {
        super(world, camera, rectangleObject);
        
        //Rotation speed
        if(rectangleObject.getProperties().get("Speed") != null)
            speed = Float.parseFloat((String) rectangleObject.getProperties().get("Speed"));
        
        body.setFixedRotation(false);
        body.setAngularVelocity(speed*MathUtils.degreesToRadians);
    }
    
    @Override
    public BodyType getBodyType(){
        return BodyType.KinematicBody;
    }
    
    @Override
    public void activate(){
        active = !active;
        
        if(active)
            body.setAngularVelocity(speed*MathUtils.degreesToRadians);
        else
            body.setAngularVelocity(0);
    }
}

About this code :

  • ObstacleRevolving extends Obstacle
  • ObstacleRevolving is formed by a KinematicBody
  • I read the properties of the TiledMapObject to determine the rotation speed...
  • ... and I apply the angular velocity
  • Finally, the activate() function called when we use an ItemSwitch to control the ObstacleRevolving sets the angular velocity to 0 or to the speed value, depending on if we turn the ObstacleRevolving on or off.
And guess what code we put in the main for loop of the TiledMapReader.java ? Yeah, you’re right :
for (RectangleMapObject rectangleObject : objects.getByType(RectangleMapObject.class)) {
            if(rectangleObject.getProperties().get("Type") != null){
                ...

                //Revolving obstacles
                else if(rectangleObject.getProperties().get("Type").equals("Revolving")){
                    ObstacleRevolving obstacle = new ObstacleRevolving(world, camera, rectangleObject);
                    obstacles.add(obstacle);
                }

                ...
            }
And here is the result !

Viewing posts 1 to 20 of 43 · Next page · Last page