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

libGDX Jam - Powered by RoboVM & Robotality

Hosted bybadlogic
1,684
Ratings
OverviewSubmissionsResults
Community163
ScreenshotsSubmission feed

Apprentice Soft's devlog

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

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.

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 inHelp --->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 thelibGDX 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 inWindow --->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 inFile --->Import.

You'll get this screen :


Click onGradle Project andNext, and you'll get this screen :


On this screen browse to the location of your project and find the foldercore. Then click onBuild Model, check all the part of the project you want to import to Eclipse (android, desktop....) knowing that you absolutely needcore, and alsodesktop 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 thecore 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 folderdesktop and go toRun 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 !

Time to code !

Basic setup

First of all, we'll modify theDesktopLauncher.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 myDesktopLauncher.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 theMyGdxGame.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 aSpriteBatch, that I'll use to draw all the graphics of the game, and anAssetManager, that will be useful later to load the assets, when I'll have assets. Then you can see that in thecreate() I callthis.setScreen(new GameScreen(this));, thus the game display the game screen. Later I'll have more screens, like aLoadingScreen that I'll display when I load the assets, and aMainMenuScreen.

Important : In therender(), don't forget to dosuper.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 useBox2D, and to create the levels, I'll useTiled. 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, calledLevel 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 !

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 aGameConstants 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 theGameConstants.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 arevery 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 theMPP andPPM 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 aTiledMapReader to read the map we created and created bodies :

Here is theTiledMapReader.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 thetiledMap and create the hero and the objects. TheTiledMapReader 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 anObstacle and we put it in anObstacle Array.

Here is the classHero.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);    }}

TheHero 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 adisplacement() method to control the hero with WAD keys. There are also few others method that could be useful during the development.

Box2D (2/2)

Here is theObstacle.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;    }}

TheObstacleclasse takes a RectangleMapObject given by theTiledMapReader 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 !!

libGDX

Finally, let's modify the GameScreen !

Here is theGameScreen.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 thecamera, with the size I define in theGameConstants.java, then I create theWorld. The lineWorld.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, thetiledMap, thetiledMapRenderer and theTiledMapReader.

Notice that thetiledMapRenderer is anOrthogonalTiledMapRendererWithSprites that I modified to take into account the Box2D conversion factor. The code will follow next.

  • In therender(), 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 thedisplacement()method to control the hero with the keyboard.

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


Here is theOrthogonalTiledMapRendererWithSprites.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 post9 years ago
Deleted7 years ago

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

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 theGameScreen.java, I'll replace theOrthographicCamera by a camera that I'll callMyCamera, that extendsOrthographicCamera.

Here is the code forMyCamera.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 inMyCamera, is thedisplacement() method :

  1. I check if the camera position (this.position.x andthis.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 is0.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.

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 theGameConstants.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 theHero 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 theHero.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 floatoxygenLevelandfuelLevel, and I initiate them with their Max values in the creator. Then, in thedisplacement(), I added

oxygenLevel -= Gdx.graphics.getDeltaTime();

which makes theoxygenLevel decrease by one unit every second. I think I should rename thedisplacement() 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, thefuelLevel decreases byGameConstants.FUEL_CONSUMPTION units every second.

In order to monitor theoxygenLevel and thefuelLevel 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 theFuelLevel doesn't decrease when no impulse is applied to the hero, while theOxygenLevel decreases all along the animation.


Finally, to be able to lose, in therender() of theGameScreen we need to add some code to check theoxygenLevel andfuelLevel, 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 therender() of theGameScreen 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 therender() of theGameScreen 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.

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, anExit class, that is a subclass from myObstacle classis created.

Here is theExit.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 anExit and anObstacle are :

  • TheExit is asensor (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.
  • Theuser 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 anObstacle and theExit.
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 newTiledMapReader.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 previousTiledMapReader.java :

  • In thefor loop we add anif, 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 ofType isExit, we create anExit, and add it to theobstacles Array
  • If the rectangle doesn’t have a property called "Type", we create anObstacle.
Finally, in the GameScreen, we set a contactListener detecting the collision between bodies
The contactListener will be in theshow() of theGameScreen.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 thebeginContact to detect collisions between theHero and theExit. 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 !

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 theObstacleLight, that is obviously a subclass ofObstacle.

Here is theObstacleLight.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 theObstacle class :

  • The UserData of the body and fixture ane "ObstacleLight" instead of "Obstacle"
  • The BodyType is aDynamicBody, which means that the body will move under the action of forces (collision, gravity...)
  • TheObstacleLight 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", theObstacleLight 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, thefor loop of theTiledMapReader.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 createdObstacleLight 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 !


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 (HeadandAxis), 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 thePistons withTiledand the result after running the code :

Creating the Piston in Tiled

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

To show that these 2Rectangles 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 2Rectangles. Of course, if we create severalPistons, for example 4Pistons, we will have Group 1, Group 2, Group 3, and Group 4.

Now let’s see the code

Here is the code forObstaclePiston.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
  • Aftersuper(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 secondFixture. 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, theactive() method applies the eventual delay before starting the motion and update the motion direction each time the ObstaclePiston reaches the end of its stroke.

Animated Obstacles : Piston (2/2)

Making the TiledMapReader recognize the ObstaclePiston

Here is the updated code forTiledMapReader.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 previousTiledMapReader.java :
  • First, we create an Array :pistons = new Array<MapObject>
  • In thefor loop, we store everyRectangles which "Type" isPiston in thepistons Array.
  • Outside the mainfor loop, we create anotherfor loop dedicated to createObstaclePiston : It will check if 2 objects in thepistons Array have the same "Group" number, create anObstaclePiston from these 2 objects before removing them from thepistons Array and adding the newly create ObstaclePiston to theobstacles Array.

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

In therender() of theGameScree.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" !

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.

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 :

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 !

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 :


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 !


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 !

Viewing posts1 to20 of 43 ·Next page ·Last page
Log in to reply Join the discussion. Log in or create a free itch.io account to continue.
itch.io on Twitteritch.io on Facebook
AboutFAQBlogContact us
Copyright © 2025 itch corp ·Directory ·Terms ·Privacy ·Cookies

[8]ページ先頭

©2009-2025 Movatter.jp