📜 ⬆️ ⬇️

Multiplayer Games: Inside Look

Hey.

Recently, I created a mobile game for Android, in which there could potentially be multiplayer, which was what the users requested.
Multiplayer was not envisaged, since it did not respect the separation of the model and the presentation.
In this article I will consider a simple implementation of the network mode of the game and talk about the mistakes made at the stage of thinking through the architecture of the game.
Inspired by the goblin wars II article, the structure of the game was divided into independent blocks, which ultimately allowed users to play over the network.

The base class for all game objects contained all the logic of the MVC model inside of him - he knew how to draw himself and change his state.
public abstract class BaseObject { public byte type; public byte state; public Rectangle body; public abstract void update(float delta); public abstract void draw(float delta); } 

Separate the model from the presentation.
Any class that will draw game objects will implement the interface.
 public interface Drawer { void draw(BaseObject obj, float delta); } 

Thinking through the architecture for multiplayer, namely the separate implementation of the client and the implementation of the server, we move the fields necessary for drawing the object into a separate class.
 public abstract class State { public byte type; public float x, y; } 

Thus, we will be able to store only the presentation on the client side, and there will be no need to create model objects.

When there were a lot of objects in the game, it became too difficult to accompany such code and add new features.
The transition to the components greatly facilitated the creation of new game objects, since in essence the creation of a new entity was a designer.
All objects in the game are inherited from the base class Entity, and the only difference is in the state returned by the getState () method and a different set of components.
An example of such a class
 public class StoneBox extends Entity { private StoneBoxState stoneBoxState = new StoneBoxState(); private SolidBodyComponent solidBody; private MapBodyComponent mapBody; public StoneBox(SorterEntityManager entityManager, float x, float y) { super(entityManager); type = EntityType.BRICK_STONE; solidBody = new SolidBodyComponent(this); solidBody.isStatic = true; solidBody.rectangle.x = x; solidBody.rectangle.y = y; mapBody = new MapBodyComponent(this); SorterComponent sorterComponent = new SorterComponent(this, entityManager); addComponent(solidBody); addComponent(mapBody); addComponent(sorterComponent); } @Override public State getState() { stoneBoxState.x = solidBody.rectangle.x; stoneBoxState.y = solidBody.rectangle.y; return stoneBoxState; } } 


The structure of the game has changed significantly, and the separation of entities allowed us to create such a bunch:
Server -> ServerImplementation <-> ClientImplementation <- Client
All the difference between a network game from a local one comes down to different implementations.
')
Server class actions every frame - it gives the actual state array for ServerImplementation and transmits to the client.
It looks like this:
 public class LocalServerImpl { ... public void update(float delta){ clientImpl.states = server.getStates(); ... } } 

The Client class each frame takes the current state from ClientImplementation , and then displays the received data on the screen.
Of course, not only states are transmitted, but also events about the beginning of the game, user commands and others. Their logic does not differ from the transfer of states.

Now for the implementation of the network mode, we need to change the implementation of ServerImplementation <-> ClientImplementation , as well as for the state classes to implement an interface for serialization and deserialization:
 public interface BinaryParser { void parseBinary(DataInputStream stream); void fillBinary(DataOutputStream stream); } 

An example of such a class
 public class StoneBoxState extends State { public StoneBoxState() { super(StateType.STONE_BOX); } @Override public void parseBinary(DataInputStream stream) { x = StreamUtils.readFloat(stream); y = StreamUtils.readFloat(stream); } @Override public void fillBinary(DataOutputStream stream) { StreamUtils.writeByte(stream, type); StreamUtils.writeFloat(stream, x); StreamUtils.writeFloat(stream, y); } } 


Why when parsing we do not read the byte responsible for the type? We read it when determining the type of an object in order to create the necessary entity from the input byte array.
State parsing
 public static State parseState(DataInputStream stream) { byte type = StreamUtils.readByte(stream); State state = null; switch (type) { case STONE_BOX: state = new StoneBoxState(); break; ... } state.parseBinary(stream); return state; } 


I used the Kryonet library to work with the network, it is very convenient if you don’t want to know how data packets are transmitted, and only the result is important to you.
LocalServerImpl replace on NetworkServerImpl , transfer data is not more difficult:
Sending Game State
  Array<State> states = entityManager.getStates(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream); StreamUtils.writeByte(dataOutputStream, GameMessages.MESSAGE_SNAPSHOT); for (int i = 0; i < states.size; i++) { states.get(i).fillBinary(dataOutputStream); } byte[] array = byteArrayOutputStream.toByteArray(); byte[] compressedData = CompressionUtils.compress(array); sendToAllUDP(compressedData); 


We get the current status, write to the array bytes, archive, send to clients.
On the client, the differences are also minimal, he will also receive the status, but they will come over the network:
Getting game status
 private void onReceivedData(byte[] data) { byte[] result = CompressionUtils.decompress(data); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(result); DataInputStream dataInputStream = new DataInputStream(byteArrayInputStream); byte messageType = StreamUtils.readByte(dataInputStream); switch (messageType) { ... case GameMessages.MESSAGE_SNAPSHOT: snapshot.clear(); try { while (dataInputStream.available() > 0) { snapshot.add(StateType.parseState(dataInputStream)); } } catch (IOException e) { e.printStackTrace(); } break; } } 


Types of messages between client and server
  public static final byte MESSAGE_PLAYER_ACTION = 0; public static final byte MESSAGE_SNAPSHOT = 1; public static final byte MESSAGE_ADD_PLAYER = 2; public static final byte MESSAGE_GAME_OVER = 3; public static final byte MESSAGE_LEVEL_COMPLETE = 4; public static final byte MESSAGE_LOADED_NEW_LEVEL = 5; public static final byte MESSAGE_CHANGE_ZOOM_LEVEL = 6; 



Any actions on the client side are first sent to the server, the server status changes, and this data is returned back to the client.
I recorded a small video that demonstrates the network mode (the libgdx engine allows you to run applications on a PC)

Source: https://habr.com/ru/post/200880/


All Articles