This is the first part of the series, which introduces the offline game. Other parts are available here:
What we’re going to make
We’ll take the 1979 arcade classic Asteroids and modify it’s premise a bit, so it becomes a deathmatch. Ironically, we’ll skip the eponymous asteroids. In the case of our game, they’ll be replaced by other players.
Choice of technologies
We’ll use Java 8, LibGDX as a game framework, Jackson for data serialisation and implementations of Socket.IO for client-server communication.
If you’re not familiar with LibGDX yet I encourage you to do their simple game tutorial before reading further, and if that got you interested, definitely dive into Udacity’s excellent Game Development with LibGDX and How to Make a Platformer Game Using LibGDX.
Reference repository
Everything that we’re going to build here is available as a code at reference repository. Please note that part of the code is omitted in this series, so it would the best if you have it tabbed somewhere.
Lonely in the Universe
We’ll start with developing an offline game and enhance it with multiplayer capabilities later. In this particular part, we’ll deal with moving a ship around and shooting bullets into the void, and we’ll add another mockup player in the future.
So, set up a fresh LibGDX project with only Desktop subproject and let’s get into it.
When LibGDX is done initializing, you’ll need to change sourceCompatibility from 1.6 to 1.8 in build.gradle files in both desktop and core subprojects. Note that if you’d want to support Android and GWT-based HTML builds you’d have to use 1.7 at most. However, for this tutorial, all we care about is the desktop. I also find lambdas and streams introduced in 1.8 very useful :).
Go ahead and delete everything inside core/src/com/asteroids/game and we’ll start developing packages from scratch. In this part, all of our development will happen inside of core subproject.
Vectors Utils
I know, I know. Utility classes are evil. But in this particular case we’re missing a utility method on a Vector2 class that we can’t augment ourselves, so it’s justified.
The Vectors class will live in util package, and it will provide a method that returns a Vector2 pointed in a direction according to the rotation. This will be very helpful in a moment when we’ll be rotating things.
public class Vectors { public static Vector2 getDirectionVector(float rotation) { return new Vector2(-(float)Math.sin(Math.toRadians(rotation)), (float)Math.cos(Math.toRadians(rotation))); } }
Don’t worry (or don’t get your hopes up) – that’s about all the math we’re going to touch.
Controls
We’ll tackle controls package next, because our upcoming models will use its interface directly. We want our controls to let us know whether the player wants to move forwards, move left, move right or shoot. This is exactly how our interface is going to look like:
public interface Controls { boolean forward(); boolean left(); boolean right(); boolean shoot(); }
For now, there will be just one implementation of that interface, and since keyboard controls make the most sense in our case, we’ll start with that. It’s quite straightforward:
import static com.badlogic.gdx.Input.Keys.*; public class KeyboardControls implements Controls { @Override public boolean forward() { return Gdx.input.isKeyPressed(UP); } @Override public boolean left() { return Gdx.input.isKeyPressed(LEFT); } @Override public boolean right() { return Gdx.input.isKeyPressed(RIGHT); } @Override public boolean shoot() { return Gdx.input.isKeyPressed(SPACE); } }
Model
Another package called model will contain our domain model classes and interfaces. Let’s take a bird’s eye look at original game’s screenshot and name them:
We’re inside of Arena. There’s a Player that owns a Ship. Ship is able to shoot Bullets.
Also, Ship and Bullet share a common trait – they’re Visible, which means they have some sort of shape and a color. This abstraction will come in handy when we’ll be about to render our game.
That gives us model classes Arena, Player, Ship and Bullet, as well as interface Visible.
Visible
This will basically be our “thing on the scene”. It will have color and a shape.
We’ll use LibGDX’s Polygons to represent underlying shapes because they’re easy to work with and have good support for what we’ll need later (rendering, collision detection).
public interface Visible { Color getColor(); Polygon getShape(); }
Arena
Arena will be responsible for keeping Visible things inside of its bounds.
Asteroids game has a particular way of dealing with arena bounds – whenever something reached one of those, it’s immediately teleported to the other side of parallel bound. For the sake of simplicity, we’ll do the easiest implementation, which moves whole objects rather than keeping them partially on both sides.
public class Arena { private final Rectangle bounds; public Arena(float width, float height) { bounds = new Rectangle(0, 0, width, height); } public void ensurePlacementWithinBounds(Visible visible) { Polygon shape = visible.getShape(); Rectangle shapeBounds = shape.getBoundingRectangle(); float x = shape.getX(); float y = shape.getY(); if(x + shapeBounds.width < bounds.x) x = bounds.width; if(y + shapeBounds.height bounds.width) x = bounds.x - shapeBounds.width; if(y > bounds.height) y = bounds.y - shapeBounds.height; shape.setPosition(x, y); } }
Player
Player will be a rather simple entity, responsible mostly for managing his Ship. Notice that at any given moment a Player might not have a Ship – for example, his Ship was just destroyed and he waits to receive another one. Therefore Ship won’t be final, and it will also be Optional.
public class Player { private final Controls controls; private final Color color; private Optional<Ship> ship; public Player(Controls controls, Color color) { this.controls = controls; this.color = color; } public void setShip(Ship ship) { this.ship = Optional.ofNullable(ship); } public void update(float delta) { ship.ifPresent(ship -> { ship.control(controls, delta); ship.update(delta); }); } public Optional<Ship> getShip() { return ship; } public Color getColor() { return color; } }
Ship
Compared to Player, Ship will be somewhat more sophisticated. It will deal with basic physics of the movement (velocity and drag) and set its underlying shape position accordingly to these coefficients.
It will also be responsible for outputting Bullets somewhere (more specifically, into the Container of Bullets, but we’ll talk more about containers later). In order to do that we need to know if it canShoot (enough time has passed since the last shot) and if itwantsToShoot (the key indicating shot was pressed).
It will be the first class we see that implements Visible interface. A common pattern for creation Visible things will be to use a set of points (vertices) to initialize its Polygon.
public class Ship implements Visible { private static final float[] VERTICES = new float[] { 0, 0, 16, 32, 32, 0, 16, 10 }; private static final float MAX_SPEED = 2000f; private static final float ACCELERATION = 500f; private static final float ROTATION = 10f; private static final float DRAG = 2f; private static final Vector2 MIDDLE = new Vector2(16, 16); private static final Vector2 BULLET_OUTPUT = new Vector2(16, 32); private static final Duration SHOT_INTERVAL = Duration.ofMillis(300); private final Player owner; private final Polygon shape; private final Vector2 velocity; private float rotationVelocity; private Instant lastShot; private boolean canShoot; private boolean wantsToShoot; public Ship(Player owner) { shape = new Polygon(VERTICES); shape.setOrigin(MIDDLE.x, MIDDLE.y); this.owner = owner; velocity = new Vector2(0, 0); lastShot = Instant.EPOCH; } public static Vector2 getMiddle() { return new Vector2(MIDDLE); } @Override public Color getColor() { return owner.getColor(); } @Override public Polygon getShape() { return shape; } public void control(Controls controls, float delta) { if(controls.forward()) moveForwards(delta); if(controls.left()) rotateLeft(delta); if(controls.right()) rotateRight(delta); wantsToShoot = controls.shoot(); } public void update(float delta) { applyMovement(delta); applyShootingPossibility(); } public Optional<Bullet> obtainBullet() { if(canShoot && wantsToShoot) { lastShot = Instant.now(); return Optional.of(new Bullet( owner, bulletStartingPosition(), shape.getRotation()) ); } return Optional.empty(); } private Vector2 getDirection() { return Vectors.getDirectionVector(shape.getRotation()); } private void moveForwards(float delta) { Vector2 direction = getDirection(); velocity.x += delta * ACCELERATION * direction.x; velocity.y += delta * ACCELERATION * direction.y; } private void rotateLeft(float delta) { rotationVelocity += delta * ROTATION; } private void rotateRight(float delta) { rotationVelocity -= delta * ROTATION; } private void applyMovement(float delta) { velocity.clamp(0, MAX_SPEED); velocity.x -= delta * DRAG * velocity.x; velocity.y -= delta * DRAG * velocity.y; rotationVelocity -= delta * DRAG * rotationVelocity; float x = delta * velocity.x; float y = delta * velocity.y; shape.translate(x, y); shape.rotate(rotationVelocity); } private void applyShootingPossibility() { canShoot = Instant.now().isAfter(lastShot.plus(SHOT_INTERVAL)); } private Vector2 bulletStartingPosition() { return new Vector2(shape.getX(), shape.getY()).add(BULLET_OUTPUT); } }
Bullet
Bullet will be our last model that we will create (and another Visible thing). It will move at a constant speed in a steady direction determined by the shooter – but within a limited range.
We’ll also store a Player that this bullet was shot by because it will be important in the foreseeable future (for instance when we know who shot the bullet that destroyed some other ship,
it’s trivial to award the shooter with points).
public class Bullet implements Visible { private static final float[] VERTICES = new float[] { 0, 0, 2, 0, 2, 2, 0, 2 }; private static final float SPEED = 500f; private static final float RANGE = 400f; private final Player shooter; private final Polygon shape; private float remainingRange; public Bullet(Player shooter, Vector2 startPosition, float rotation) { shape = new Polygon(VERTICES); shape.setPosition(startPosition.x, startPosition.y); shape.setRotation(rotation); shape.setOrigin(0, -Ship.getMiddle().y); this.shooter = shooter; remainingRange = RANGE; } @Override public Color getColor() { return shooter.getColor(); } @Override public Polygon getShape() { return shape; } public void move(float delta) { Vector2 direction = Vectors.getDirectionVector(shape.getRotation()); Vector2 movement = new Vector2(direction.x * delta * SPEED, direction.y * delta * SPEED); remainingRange -= movement.len(); shape.translate(movement.x, movement.y); } public boolean isInRange() { return remainingRange > 0; } }
Container
So we have our models and that’s neat, but soon enough we’ll going to want to perform operations on groups of them. One such operation was already hinted in the Ship – whenever it shots, it has to output a Bullet somewhere. The most natural thing to do is to put it among other Bullets. That’s the basic notion of a Container – it holds on t a group of Things, which makes it easy to perform batch operations on them. Every Container inside this package will implement this interface:
public interface Container<Thing> { void add(Thing toAdd); List<Thing> getAll(); default Stream<Thing> stream() { return getAll().stream(); } void update(float delta); }
Bullets Container
For now this will be our only Container, but more will come. It’s main responsibility will be to hold on to all Bullets that’ve been shot, update them and remove those that went out of range.
public class BulletsContainer implements Container<Bullet> { private final List<Bullet> bullets; public BulletsContainer(List<Bullet> bullets) { this.bullets = bullets; } public BulletsContainer() { this(new ArrayList()); } @Override public void add(Bullet bullet) { bullets.add(bullet); } @Override public List<Bullet> getAll() { return bullets; } @Override public void update(float delta) { bullets.forEach(bullet -> bullet.move(delta)); bullets.removeIf(bullet -> !bullet.isInRange()); } }
Rendering
Separation of rendering and business logic is important in programming in general, but it gets especially important in multiplayer games. We’ll talk more about it in upcoming parts, for now let’s keep our rendering neatly separated from our models and containers. To achieve this goal we’ll introduce a Renderer – an object that will hold on to another object it knows how to render, and given a right tool (which in our case will be LibGDX’s ShapeRenderer) is able to put pixels on the screen for us.
public interface Renderer { void render(ShapeRenderer shapeRenderer); }
Visible Renderer
First natural candidate for having it’s own Renderer is anything that implements Visible interface, as we can directly obtain all the information needed to render it directly from Visible itself.
public class VisibleRenderer implements Renderer { private final Visible visible; public VisibleRenderer(Visible visible) { this.visible = visible; } @Override public void render(ShapeRenderer shapeRenderer) { shapeRenderer.setColor(visible.getColor()); shapeRenderer.polygon(visible.getShape().getTransformedVertices()); } }
Player Renderer
Player is a peculiar entity, given that its Ships can change and at times be unavailable. We’ll need a VisibleRenderer for current Ship, and we’d also like Renderers that hold on to previous Ships to be garbage collected. We don’t want to manage all this complexity directly inside of a game loop, so we’ll create a dedicated PlayerRenderer. Note the default usage of WeakHashMap which will discard entries dedicated for Ships that do not exist anymore.
public class PlayerRenderer implements Renderer { private final Map<Ship, Renderer> cache; private final Player player; public PlayerRenderer(Player player, Map<Ship, Renderer> cache) { this.player = player; this.cache = cache; } public PlayerRenderer(Player player) { this(player, new WeakHashMap<>()); } @Override public void render(ShapeRenderer shapeRenderer) { player.getShip().ifPresent(ship -> cache .computeIfAbsent(ship, VisibleRenderer::new) .render(shapeRenderer)); } }
Container Renderer
Lastly, we’ll need a generic Renderer for Containers. Given the variadic nature of their content which can disappear at any given moment (for example Bullets can go out of range), we’ll use similar caching mechanism as in Player renderer. We’ll also externalise creation of underlying Renderers to upper layer because we can’t possibly know how to instantiate them all here.
public class ContainerRenderer<Thing> implements Renderer { private final Container<Thing> container; private final Function<Thing, Renderer> rendererFactory; private final Map<Thing, Renderer> cache; public ContainerRenderer(Container<Thing> container, Function<Thing, Renderer> rendererFactory, Map<Thing, Renderer> cache) { this.container = container; this.rendererFactory = rendererFactory; this.cache = cache; } public ContainerRenderer(Container<Thing> container, Function<Thing, Renderer> rendererFactory) { this(container, rendererFactory, new WeakHashMap<>()); } @Override public void render(ShapeRenderer shapeRenderer) { container.stream().forEach(thing -> cache .computeIfAbsent(thing, rendererFactory) .render(shapeRenderer)); } }
Putting it all together
We now have all the ingredients ready to assemble the first version of our game. One last decision to make is to determine our world’s size. Let’s go with 800×600. It will also be the size of the viewport we’ll use. Let’s initialize dependencies in the Game class and then inject them into the Screen. You can see that these classes will serve as the integration layer for everything we’ve done up until this point.
public class AsteroidsGame extends Game { public static final float WORLD_WIDTH = 800f; public static final float WORLD_HEIGHT = 600f; private Screen asteroids; @Override public void create() { Viewport viewport = new FillViewport(WORLD_WIDTH, WORLD_HEIGHT); ShapeRenderer shapeRenderer = new ShapeRenderer(); Arena arena = new Arena(WORLD_WIDTH, WORLD_HEIGHT); Player player = new Player(new KeyboardControls(), Color.WHITE); Container<Bullet> bulletsContainer = new BulletsContainer(); player.setShip(new Ship(player)); PlayerRenderer playerRenderer = new PlayerRenderer(player); ContainerRenderer<Bullet> bulletsRenderer = new ContainerRenderer<>(bulletsContainer, VisibleRenderer::new); asteroids = new AsteroidsScreen( viewport, shapeRenderer, arena, player, bulletsContainer, playerRenderer, bulletsRenderer); setScreen(asteroids); } @Override public void dispose() { asteroids.dispose(); } }
public class AsteroidsScreen extends ScreenAdapter { private final Viewport viewport; private final ShapeRenderer shapeRenderer; private final Arena arena; private final Player player; private final Container<Bullet> bulletsContainer; private final Renderer playerRenderer; private final ContainerRenderer<Bullet> bulletsRenderer; public AsteroidsScreen( Viewport viewport, ShapeRenderer shapeRenderer, Arena arena, Player player, Container<Bullet> bulletsContainer, Renderer playerRenderer, ContainerRenderer<Bullet> bulletsRenderer) { this.viewport = viewport; this.shapeRenderer = shapeRenderer; this.arena = arena; this.player = player; this.bulletsContainer = bulletsContainer; this.playerRenderer = playerRenderer; this.bulletsRenderer = bulletsRenderer; } @Override public void render(float delta) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); player.update(delta); player.getShip() .ifPresent(arena::ensurePlacementWithinBounds); player.getShip() .flatMap(Ship::obtainBullet) .ifPresent(bulletsContainer::add); bulletsContainer.update(delta); bulletsContainer.stream() .forEach(arena::ensurePlacementWithinBounds); viewport.apply(); shapeRenderer.setProjectionMatrix(viewport.getCamera().combined); shapeRenderer.begin(ShapeRenderer.ShapeType.Line); playerRenderer.render(shapeRenderer); bulletsRenderer.render(shapeRenderer); shapeRenderer.end(); } @Override public void resize(int width, int height) { viewport.update(width, height, true); } @Override public void dispose() { shapeRenderer.dispose(); } }
In the game loop (render method) we follow a pattern of updating first and then rendering.
Pause
Let’s reflect on what we’ve accomplished so far. Go ahead and run desktop project in Gradle (you can do that quickly with ./gradlew desktop:run command on MacOS/Linux or gradlew.bat desktop:run on Windows). It should look something like this:
If it doesn’t, compare your code to the reference material and try to spot the difference.
Cool, so that’s a start. Now let’s introduce another player. Albeit offline and immobile, it will be the basis for our upcoming support for multiple players and client-server architecture.
Players Container
So far we’ve been dealing with just one player, but now our needs call for another Container – a PlayersContainer. It’ll have a bit more going on inside compared to BulletsContainer – we’ll include methods for streaming Player‘s Ships and streaming their obtained Bullets.
public class PlayersContainer implements Container<Player> { private final List<Player> players; public PlayersContainer(List<Player> players) { this.players = players; } public PlayersContainer() { this(new ArrayList<>()); } @Override public void add(Player toAdd) { players.add(toAdd); } @Override public List<Player> getAll() { return players; } @Override public void update(float delta) { players.forEach(player -> player.update(delta)); } public Stream<Ship> streamShips() { return stream() .map(Player::getShip) .filter(Optional::isPresent) .map(Optional::get); } public Stream<Bullet> obtainAndStreamBullets() { return streamShips() .map(Ship::obtainBullet) .filter(Optional::isPresent) .map(Optional::get); } }
A place to start
We haven’t determined yet where the Ship should be introduced to the world. For the sake of quick feedback Polygon‘s default (0, 0) starting point was good enough, but now we’ll add starting position to the Ship:
public Ship(Player owner, Vector2 startingPosition, float startingRotation) { ... shape.setPosition(startingPosition.x, startingPosition.y); shape.setRotation(startingRotation); ... }
A way to get there
In the actual game, the situation where a player gets his ship blasted by another player and needs a new one will happen often. That means we will need another entity that will take care of respawning ships – which naturally points as towards a Respawner name. It’s neither a model nor a container. It’s a managing entity, hence we will introduce a new manager package.
Our Respawner will have to know everything needed to determine whether a Player needs a new Ship and how to instantiate it. Its logic will be quite simple: scan through Players, find ones without Ships and give them a new one at some random point inside of arena bounds.
public class Respawner { private static final Random random = new Random(); private final Container<Player> playersContainer; private final float widthBound; private final float heightBound; public Respawner(Container<Player> playersContainer, float widthBound, float heightBound) { this.playersContainer = playersContainer; this.widthBound = widthBound; this.heightBound = heightBound; } public void respawn() { playersContainer.stream() .filter(player -> !player.getShip().isPresent()) .forEach(player -> player.setShip(new Ship(player, randomRespawnPoint(), 0))); } private Vector2 randomRespawnPoint() { return new Vector2(random.nextInt(Math.round(widthBound)), random.nextInt(Math.round(heightBound))); } }
Noop Controls
Our immobile Player needs to be instantiated with Controls nonetheless, so let’s create an implementation that prevents him from moving:
public class NoopControls implements Controls { @Override public boolean forward() { return false; } @Override public boolean left() { return false; } @Override public boolean right() { return false; } @Override public boolean shoot() { return false; } }
Blast Away
There’s one last thing we’ll implement before the network part: registering hits between the Bullets and the Players.
Collision Detection
A fundamental part of registering hits is the ability to tell when Bullet and Player collided. There are whole books written on the subject, but fortunately, we’re standing on the shoulders of a giant (LibGDX), so the algorithmic work has already been done. Intuitively, being Visible means you can collide with something, so let’s add a default method to our Visible interface:
public interface Visible { ... default boolean collidesWith(Visible anotherVisible) { return Intersector.overlapConvexPolygons(this.getShape(), anotherVisible.getShape()); } }
Yup, that’s it. Collision detection in one line. LibGDX’s Intersector works great with Polygons and we’ll take full advantage of that here.
Handling hits
We’ll need to introduce little changes on our models, making them react to collisions. In the case of Bullet it’s going to mark it for future removal. In the case of Player it will be removing his Ship.
public class Bullet implements Visible { ... private boolean hasHitSomething; ... public void noticeHit() { hasHitSomething = true; } public boolean hasHitSomething() { return hasHitSomething; } }
public class Player { ... public Player(Controls controls, Color color) { ... this.ship = Optional.empty(); } public void setShip(Ship ship) { this.ship = Optional.of(ship); } public void noticeHit() { this.ship = Optional.empty(); } ... }
We’ll also need to remove Bullets that have hit something. It’s going to be another job of BulletsContainer:
public class BulletsContainer implements Container<Bullet> { ... @Override public void update(float delta) { ... bullets.removeIf(bullet -> !bullet.isInRange() || bullet.hasHitSomething()); } }
Hadron Players and Bullets Collider
Now that we have our bits and pieces of collision-related logic spread across the code, let’s wrap them up with another manager entity – a Collider. As name suggest, it’s going to orchestrate collision checks between objects and appropriately notify them if such collision occurs.
public class Collider { private final Container<Player> playersContainer; private final Container<Bullet> bulletsContainer; public Collider(Container<Player> playersContainer, Container<Bullet> bulletsContainer) { this.playersContainer = playersContainer; this.bulletsContainer = bulletsContainer; } public void checkCollisions() { bulletsContainer.stream() .forEach(bullet -> playersContainer.stream() .filter(player -> player.getShip().isPresent()) .filter(player -> player.getShip().get().collidesWith(bullet)) .findFirst() .ifPresent(player -> { player.noticeHit(); bullet.noticeHit(); })); } }
Putting it back together again
Remember AsteroidsGame and AsteroidsScreen? We’re going to revisit them now and add everything new we’ve made. PlayersContainer, Respawner, Collider as well call methods to update their state in render.
First, let’s update dependencies graph in AsteroidsGame. Note that apart from a few new objects, we’ll also change the way Players are rendered. Now they’re in a Container, so we’ll use ContainerRenderer and pass PlayerRenderer as underlying renderer.
public class AsteroidsGame extends Game { ... @Override public void create() { Viewport viewport = new FillViewport(WORLD_WIDTH, WORLD_HEIGHT); ShapeRenderer shapeRenderer = new ShapeRenderer(); Arena arena = new Arena(WORLD_WIDTH, WORLD_HEIGHT); Player player1 = new Player(new KeyboardControls(), Color.WHITE); Player player2 = new Player(new NoopControls(), Color.LIGHT_GRAY); Container<Bullet> bulletsContainer = new BulletsContainer(); PlayersContainer playersContainer = new PlayersContainer(); playersContainer.add(player1); playersContainer.add(player2); Respawner respawner = new Respawner(playersContainer, WORLD_WIDTH, WORLD_HEIGHT); Collider collider = new Collider(playersContainer, bulletsContainer); ContainerRenderer<Bullet> bulletsRenderer = new ContainerRenderer(bulletsContainer, VisibleRenderer::new); ContainerRenderer<Player> playersRenderer = new ContainerRenderer(playersContainer, PlayerRenderer::new); asteroids = new AsteroidsScreen( viewport, shapeRenderer, playersContainer, bulletsContainer, arena, respawner, collider, playersRenderer, bulletsRenderer); setScreen(asteroids); } ... }
Having initialized our dependencies, we can use them in AsteroidsScreen:
public class AsteroidsScreen extends ScreenAdapter { private final Viewport viewport; private final ShapeRenderer shapeRenderer; private final Arena arena; private final PlayersContainer playersContainer; private final Container<Bullet> bulletsContainer; private final Respawner respawner; private final Collider collider; private final ContainerRenderer<Player> playersRenderer; private final ContainerRenderer<Bullet> bulletsRenderer; public AsteroidsScreen( Viewport viewport, ShapeRenderer shapeRenderer, PlayersContainer playersContainer, Container<Bullet> bulletsContainer, Arena arena, Respawner respawner, Collider collider, ContainerRenderer<Player> playersRenderer, ContainerRenderer<Bullet> bulletsRenderer) { this.viewport = viewport; this.shapeRenderer = shapeRenderer; this.arena = arena; this.respawner = respawner; this.collider = collider; this.playersContainer = playersContainer; this.bulletsContainer = bulletsContainer; this.playersRenderer = playersRenderer; this.bulletsRenderer = bulletsRenderer; } @Override public void render(float delta) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); respawner.respawn(); collider.checkCollisions(); playersContainer.update(delta); playersContainer.streamShips() .forEach(arena::ensurePlacementWithinBounds); playersContainer.obtainAndStreamBullets() .forEach(bulletsContainer::add); bulletsContainer.update(delta); bulletsContainer.stream() .forEach(arena::ensurePlacementWithinBounds); viewport.apply(); shapeRenderer.setProjectionMatrix(viewport.getCamera().combined); shapeRenderer.begin(ShapeRenderer.ShapeType.Line); playersRenderer.render(shapeRenderer); bulletsRenderer.render(shapeRenderer); shapeRenderer.end(); } ... }
That concludes our offline part. In the next part, we’ll introduce server infrastructure and get ready to start sending some data.
Now, take a minute to run what’s been done so far and enjoy :).
If you are ready for the next part, try part 2 of Developing Lag Compensated Multiplayer Game – this time we set up the server.