Multiplayer in games: an inside look

Hey.

I recently created a mobile game for Android, which could potentially have multiplayer, which is what users requested.
Multiplayer was not provided, because it did not comply with the separation of model and 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 article goblin wars II, the structure of the game was divided into independent blocks, which ultimately allowed users to play on the network.

The base class for all game objects contained all the logic of the MVC model within itself - it was able to draw itself and change its 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 view.
Any class that draws game objects will implement an interface.
public  interface Drawer {
	void draw(BaseObject obj, float delta);
}

Thinking over the architecture for the multiplayer, namely, separately implementing the client and implementing the server, we take out the fields necessary for drawing the object into a separate class.
public abstract class State {
	public byte type;
	public float x, y;
}

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

When there are a lot of objects in the game, it became too difficult to maintain such a code and add new features.
The transition to components greatly facilitated the creation of new game objects, since in essence the creation of a new entity was a constructor.
All objects in the game are inherited from the Entity base class, and the only difference is in the state returned by the getState () method and a different set of components.
Example of this 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 bundle:
Server -> ServerImplementation <-> ClientImplementation <- Client
All the difference between a network game from a local one comes down to different implementations.

The actions of the server class are each frame - it gives the current array of states for ServerImplementationand passes to the client.
it looks like this:
public class LocalServerImpl {
	...
	public void update(float delta){
		clientImpl.states = server.getStates();
		...
	}
}

The class Clienteach frame takes the current state of y ClientImplementation, and then displays the received data on the screen.
Of course, not only states are transmitted, but also events about the start of the game, user commands and others. Their logic is no different from state transfer.

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

Example of this 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 at parsing we do not read the byte responsible for type? We read it when determining the type of object in order to create the entity we need 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 do not want to know how data packets are transmitted, and only the result is important to you.
LocalServerImplreplace with NetworkServerImpl, data transfer is not more difficult:
Sending game status
	Array 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 it to the byte array, archive it, send it to clients.
On the client, the differences are also minimal, it will also receive states, but they will already 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 state on the server changes, and this data is returned back to the client.
I recorded a short video that demonstrates the network mode (the libgdx engine allows you to run applications on a PC)

Also popular now: