Skip to content

Networking

Vrekt edited this page Apr 12, 2024 · 5 revisions

Netty

Netty client networking is kept within LunarClientServer. In there you can provide custom bootstrap, adapters, handlers, decoders, encoders, ssl context and more.

Create a client server

final int protocolVersion = 1;
final String protocolName = "MyProtocol";

// true? -> protocol defaults are initialized
final GdxProtocol myProtocol = new GdxProtocol(protocolVersion, protocolName, true);
// default bootstrap
final LunarClientServer clientServer = new LunarClientServer(myProtocol, "localhost", 5555);
// or, provide your own
final Bootstrap myBootstrap = ...
final LunarClientServer clientServer = new LunarClientServer(myProtocol, myBootstrap, "localhost", 5555);

That is your base client server initialized. There is already default encoders, decoders and handlers. These are:

Class Type Purpose
FirstInboundConnectionHandler Adapter Handles the first initial connection and basic authentication, very optional
ServerProtocolPacketDecoder Decoder Handles decoding packets from the server and sending them to the handler
ProtocolPacketEncoder Encoder Basic encoder that will encode any packet
PlayerConnectionHandler Handler The default handler for all player connections

if you wish you can provide your own encoders and decoders like so:

clientServer.setProtocolDecoder(new MyProtocolDecoder());
clientServer.setProtocolEncoder(new MyProtocolEncoder());

By default, when the client connects to a remote server a connection handler will be created. From here is where you send and receive packets. You can access this by using

clientServer.getConnection();

The default one that is created is PlayerConnectionHandler which extends AbstractConnectionHandler, of course in custom implementations you can choose which one you want to extend. PlayerConnectionHandler includes functions for overriding default behaviour and some boilerplate handling for packets (for example position updates, spawning players, etc.)

With that its relatively easy to spin up a quick multiplayer game, of course for more complex things you should just write your own implementation and override as needed.

Here are just a few methods included within AbstractConnectionHandler for convenience sake.

connection.updatePosition(x, y, rotation);
connection.joinWorld("MyWorld", "myUsername", currentTime);
connection.updateWorldHasLoaded(); // indicates to the server the client has finished loading the world

As mentioned earlier, you can register your custom connection handlers like so:

clientServer.setProvider(channel -> new MyPlayerConnection(channel, protocol));

With that here is a peek at a basic custom player connection handler that will handle my custom packets I add later on.

public class MyPlayerConnection extends PlayerConnectionHandler {

    private final OasisPlayer player;

    public MyPlayerConnection(Channel channel, GdxProtocol protocol, OasisPlayer player) {
        super(channel, protocol);
        this.player = player;

        registerPacket(ServerPacketSpawnEntity.ID, ServerPacketSpawnEntity::new, this::handleSpawnEntity);
    }

    private void handleSpawnEntity(ServerPacketSpawnEntity packet) {
        // etc.
    }
}

You can also just override default behaviour if you choose to not use your own PlayerConnectionHandler. For example:

// lets override the default behaviour of the position packet, I want to log positions for debugging purposes
// this will be invoked on the main game thread instead of the network one
connection.registerHandlerSync(S2CPacketPlayerPosition.ID, packet -> {
    final S2CPacketPlayerPosition position = (S2CPacketPlayerPosition) packet;
    Gdx.app.log("Connection", "Position from player %d: %d,%d".formatted(position.getEntityId(), position.getX(), position.getY()));
    // pass this off for normal processing
    updatePlayerPosition(position);      
})

You can also choose to execute the handler async, which just runs on the current network thread instead of the main game one.

connection.registerHandlerAsync(...);

Packets

If you need to send a packet:

// immediate
connection.sendImmediately(new C2SPacketPlayerPosition(x, y, rotation));
// packet is queued
connection.send(new C2SPacketPlayerPosition(x, y, rotation));

You can customize the interval for when the queue is cleared with:

// in milliseconds
connection.setUpdateInterval(50);

With custom connection handlers you will need to register your own packets as-well.

connection.registerPacket(MyPacket.ID, MyPacket::new, this::handleMyPacket);

Protocol

With GdxProtocol you can change the handlers for server and client packets, and as shown earlier, register custom ones.

If using default decoders the max packet length allowed is 65536. Depending on complexity and data size you may want to change this

protocol.setMaxPacketFrameLength(1023291);

This should be set before the creation of any established connection.

Registering client and server packets is different from custom ones, the server and client packets should invoke the function handle(GamePacket packet) in ClientPacketHandler and ServerPacketHandler respectively.

protocol.registerClientPacket(SomePacket.ID, (buffer, handler) -> SomePacket.handle(buffer, handler));
// impl
class SomePacket {
    static void handle(ByteBuf buffer, ClientPacketHandler handler) {
        handler.handle(new SomePacket(buffer));
    }
}

With that you could implement the function handle and include a switch statement for each of your custom packets. Though, creating your own connection provider is a simpler? more elegant solution.