When I was a kid we had 4 channels, RTE 1 & 2, TV3 & TG4. Or as the kids in school would say: Poverty 1,2,3 & 4. But then we were blessed with SkyTV, 100s of channels, shows, music videos & documentaries. But the best part, we now also had games on the TV.
One of these games we really enjoyed was called Fathom. You control a little submarine that needs to slowly make its way through underwater caves. If you hit a wall, you either lose a chunk of health,.. or die. A simple idea but we really liked it. I decided to remake this game, but give it a new look.
I wanted the game to have the same dark tones as games like LIMBO or Badlands, where the foreground and player are darker, with only the background providing light.
This was simply a case of using black textures for the map pieces and player sprite.
I wanted the player to have a heavy feeling when moving to give the impression that you are moving a heavy ship around the deep sea. I achieved this by setting the density value when creating the player body in Box2d.
public static Body createBoxBody(Vector2 pos, Vector2 size, float angleInRadians, short categoryBits, short maskBits, String userData, boolean isDynamic, World world) {
BodyDef bodyDef = new BodyDef();
bodyDef.type = isDynamic ? BodyDef.BodyType.DynamicBody : BodyDef.BodyType.StaticBody;
bodyDef.position.set(pos.x / PPM, pos.y / PPM);
bodyDef.angle = angleInRadians;
Body body = world.createBody(bodyDef);
PolygonShape shape = new PolygonShape();
shape.setAsBox(size.x / 2f / PPM, size.y / 2f / PPM);
FixtureDef fixtureDef = new FixtureDef();
fixtureDef.shape = shape;
fixtureDef.density = 0.01f;
fixtureDef.filter.categoryBits = categoryBits;
fixtureDef.filter.maskBits = maskBits;
body.createFixture(fixtureDef).setUserData(userData);
shape.dispose();
return body;
}
I also added some delay when rotating the ship, so the player is required to preemptively navigate the ship in the right direction before colliding with the walls.
GIF from my project
When creating the smoke effect for the ship, my initial instinct was to go with the LibGDX particle editor, but due to version issues, I just made my own SmokeTrail class.
public class SmokeTrail {
private Player player;
private final Array<Particle> smokeBoosts;
private final int smokeDelay = 2;
private int smokeCounter = 0;
private final float opacity = 1f;
public SmokeTrail(Player player) {
this.player = player;
smokeBoosts = new Array<>();
for(int i = 0; i < 30; i++) {
Particle p = new Particle();
p.sprite = new Sprite(AssetLoader.getTexture("particle.png"));
p.sprite.setSize(40, 40);
p.sprite.setColor(1, 1, 1, 0);
smokeBoosts.add(p);
}
}
public void addSmoke(float x, float y, Vector2 dir) {
smokeCounter++;
if(smokeCounter < smokeDelay) return;
for(Particle p : smokeBoosts) {
if(p.sprite.getColor().a <= 0) {
p.sprite.setOriginCenter();
p.sprite.setPosition(x + Player.WIDTH/2f - p.sprite.getWidth()/2f, y + Player.HEIGHT/2f - p.sprite.getWidth()/2f);
p.sprite.setColor(1, 1, 1, opacity);
p.sprite.setScale(1);
p.dir = dir.cpy();
smokeCounter = 0;
break;
}
}
}
public void update() {
for(Particle smoke: smokeBoosts) {
if(smoke.sprite.getColor().a <= 0) continue;
float alpha = smoke.sprite.getColor().a-=0.03f;
smoke.sprite.setColor(1, 1, 1, alpha);
if(GameManager.playerCount > 1) {
if(player.getPlayerNum() == 0) {
smoke.sprite.setColor(1, 0.8f, 0.8f, alpha);
} else {
smoke.sprite.setColor(0.9f, 0.85f, 1, alpha);
}
}
float x = smoke.sprite.getX();
float y = smoke.sprite.getY();
smoke.sprite.setScale(smoke.sprite.getScaleX()-0.03f);
smoke.sprite.setPosition(x+smoke.dir.y*4f, y-smoke.dir.x*4f);
}
}
public void render(SpriteBatch sb) {
for(Particle smoke: smokeBoosts) {
smoke.sprite.draw(sb);
}
}
}
For creating the collision shapes for the walls, I setup a super-basic object editor to draw the polylines around the map piece images and save them as JSON. While this required some manual work to setup, the map pieces were created to be reusable, so once a piece was setup with collisions, it could be used multiple times throughout the map.
Since this game was an entry for the Speedrun Game Jam, I setup a timer in the game to record how long it takes the player to complete each level. This was a lot of fun to test as each time I tried to see how long it took to beat my best time.
While this project was fun, creating the object editor and map editor took a lot of time. This would not have been necessary had I gone with a game engine like Unity or Godot as they provide a UI for positioning objects, mapping collisions and laying out levels. It made me rethink using LibGDX for games like this and got me looking into Godot for my next project.