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.
-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?
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 :
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.
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 !
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.
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 !
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 !
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.
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.
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;}
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.
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 !!
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(); }}
Notice that thetiledMapRenderer is anOrthogonalTiledMapRendererWithSprites that I modified to take into account the Box2D conversion factor. The code will follow next.
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 !
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 :
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.
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 :
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 :
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.
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 :
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 :
@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 :
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 :
fixtureDef.densityand 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 !
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 :
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 :
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" !
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 :
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 :
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.
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 :
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 :
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 !
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 :
To use theItemSwitch, with theObstacles, I need to do some modifications in Obstacle.java:
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 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 :
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 !
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 :
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 !