Skip to content

Quick Start Guide

Vrekt edited this page Mar 1, 2023 · 3 revisions

A (somewhat) concise tutorial on how to get up and running quick. You can check out a pre-made example I use for testing here

Step 1: Create your player implementation

You can start by defining your custom player. You can either use LunarPlayer or extend that and have your own implementation.

In the example below I add a few cases for handling movement logic, a few configuration options, and rendering.

public final class MyPlayer extends LunarPlayer {

    public MyPlayer(boolean initializeComponents, TextureRegion playerTexture) {
        super(initializeComponents);

        setMoveSpeed(6.0f);
        setHasMoved(true);
        setNetworkSendRatesInMs(10, 10);
        setIgnorePlayerCollision(true);

        // default player texture
        putRegion("player", playerTexture);

        // default player configuration
        setSize(16, 16, (1 / 16.0f));
    }

    @Override
    public void update(float delta) {
        super.update(delta);
    }

    @Override
    public void pollInput() {
        setVelocity(0.0f, 0.0f, false);

        if (Gdx.input.isKeyPressed(Input.Keys.W)) {
            rotation = 0f;
            setVelocity(0.0f, moveSpeed, false);
        } else if (Gdx.input.isKeyPressed(Input.Keys.S)) {
            rotation = 1f;
            setVelocity(0.0f, -moveSpeed, false);
        } else if (Gdx.input.isKeyPressed(Input.Keys.A)) {
            rotation = 2f;
            setVelocity(-moveSpeed, 0.0f, false);
        } else if (Gdx.input.isKeyPressed(Input.Keys.D)) {
            rotation = 3f;
            setVelocity(moveSpeed, 0.0f, false);
        } else if (Gdx.input.isKeyPressed(Input.Keys.I) && !instance) {
            instance = true;
            getConnection().sendImmediately(new CPacketEnterInstance(22));
        }

    }

    @Override
    public void render(SpriteBatch batch, float delta) {
        batch.draw(getRegion("player"), getInterpolated().x, getInterpolated().y, getWidthScaled(), getHeightScaled());
    }

    @Override
    public <P extends LunarEntityPlayer, N extends LunarNetworkEntityPlayer, E extends LunarEntity> void spawnEntityInWorld(LunarWorld<P, N, E> world, float x, float y) {
        super.spawnEntityInWorld(world, x, y);
        body.setFixedRotation(true);
    }
}

Step 2: Initialize your new player

player = new MyPlayer(true, myPlayerTexture);
player.setEntityName("PlayerUsername");

The basic constructor just initializes default entity components. These are:

new EntityPropertiesComponent(); -> Username, entity ID, size, speed, health, etc.
new EntityPositionComponent(); -> Position data
new EntityVelocityComponent(); -> Velocity data
new EntityWorldComponent(); -> Information about the current world player is in

Step 3: Create a new networked world

Lunar provides a default adapter for worlds, its WorldAdapter. This also implements ScreenAdapter so you can call setScreen(world) as-well (if you wish).

Inside there you can define your own custom logic for updating and rendering the world. Alot of the ground work is already done for you. In the example below I choose to handling players joining and leaving.

public final class MultiplayerGameWorld extends WorldAdapter {

    BasicMultiplayerDemoGame game;

    public MultiplayerGameWorld(LunarPlayer player, World world, BasicMultiplayerDemoGame game) {
        super(player, world);
        this.game = game;
    }

    @Override
    public void renderWorld(SpriteBatch batch) {
        // This is not used here, but you could. Regardless we handle all rendering in main game loop.
    }

    /**
     * Handle local player joining a new world.
     *
     * @param world the new world packet
     */
    public void handleWorldJoin(SPacketJoinWorld world) {
        Gdx.app.log(BasicMultiplayerDemoGame.TAG, "Joining local-world: " + world.getWorldName() + ", entity ID is " + world.getEntityId());
        // set our player's entity ID from world packet.
        player.setEntityId(world.getEntityId());
        // spawn local player in world
        player.spawnEntityInWorld(this, 0.0f, 0.0f);
        // load into the world!
        // tell the server we are good to go.
        player.getConnection().updateWorldLoaded();
        // etc...
        game.ready = true;
    }

    /**
     * Handle a network player joining the local world
     *
     * @param packet the join packet
     */
    public void handlePlayerJoin(SPacketCreatePlayer packet) {
        Gdx.app.log(BasicMultiplayerDemoGame.TAG, "Spawning new player " + packet.getUsername() + ":" + packet.getEntityId());

        final LunarPlayerMP player = new LunarPlayerMP(true);
        // load player assets.
        player.putRegion("player", game.getTexture());
        // ignore collisions with other players
        player.setIgnorePlayerCollision(true);
        player.setProperties(packet.getUsername(), packet.getEntityId());
        // set your local game properties
        player.setSize(16, 16, (1 / 16.0f));
        // spawn player in your local world.
        player.spawnEntityInWorld(this, packet.getX(), packet.getY());
    }

    /**
     * Handle a network player leaving
     *
     * @param packet the packet
     */
    public void handlePlayerLeave(SPacketRemovePlayer packet) {
        Gdx.app.log(BasicMultiplayerDemoGame.TAG, "Player " + packet.getEntityId() + " left.");
        if (hasNetworkPlayer(packet.getEntityId())) {
            removeEntityInWorld(packet.getEntityId(), true);
        }
    }

}

As you notice, creating a new NetworkPlayer is just like initializing a new local player, its all the same. Later you will see us utilize the handleJoinWorld function. In that example we make sure our player has their entity ID and they are spawned in the correct world.

Step 4: Initialize your new world

Worlds internally use Box2d worlds, as such you must provide one when creating a new instance. For this example I simply use new World(Vector2.ZERO, true) which in turn creates a new world with no gravity and allows the world to sleep.

        // initialize the world with 0 gravity
        world = new MultiplayerGameWorld(player, new World(Vector2.Zero, true), this);
        // add default world systems
        world.addWorldSystems();
        // ignore player collisions
        world.addDefaultPlayerCollisionListener();

Adding default world systems basically just adds a entity movement system for entities and network players. Adding default player collision listener will listen for players colliding with each-other. If the player has collision turned off then it will not activate.

Step 5: Connect to the remote server

Here we can define our protocol and initialize a new client server.

// initialize our default protocol and connect to the remote server,
        final LunarProtocol protocol = new LunarProtocol(true);
        final LunarClientServer server = new LunarClientServer(protocol, "localhost", 6969);
        // set provider because we want {@link PlayerConnectionHandler}
        server.setConnectionProvider(channel -> new PlayerConnectionHandler(channel, protocol));
        final boolean result = server.connectNoExceptions();

        // failed to connect, so exit() out.
        if (server.getConnection() == null || !result) {
            Gdx.app.exit();
            return;
        }

        // retrieve our players connection and create a new world and local player.
        Gdx.app.log(TAG, "Connected to the server successfully.");
        final PlayerConnectionHandler connection = (PlayerConnectionHandler) server.getConnection();
        player.setConnection(connection);

Above we create a new server listening on localhost and a port of 6969. If the connection fails we simply exit the game.

Next, we specify the type of ConnectionProvider we want. A ConnectionProvider just provides a new PlayerConnection for us. You can choose to implement your own or use the default PlayerConnectionHandler.

Inside PlayerConnectionHandler it contains logic for updating position, velocity, forces, authentication, world joining, and, overrides for handling a packet yourself or enabling/disabling options.

With PlayerConnectionHandler you can choose what you want Lunar to handle and what you want to handle. You will see an example of this further below.

Once we are connected we make sure to get an instance of our PlayerConnection. Then, we pass it to our player so they can send network requests.

Now, we can provide those custom handling options we want to enable. In the World example below we had custom logic for handling new players and world joining.

        // enable options we want Lunar to handle by default.
        connection.enableOptions(
                ConnectionOption.HANDLE_PLAYER_POSITION,
                ConnectionOption.HANDLE_PLAYER_VELOCITY,
                ConnectionOption.HANDLE_AUTHENTICATION,
                ConnectionOption.HANDLE_PLAYER_FORCE);

        // register handlers we want to process ourselves instead of the default player connection
        connection.registerHandlerSync(ConnectionOption.HANDLE_JOIN_WORLD, packet -> world.handleWorldJoin((SPacketJoinWorld) packet));
        connection.registerHandlerSync(ConnectionOption.HANDLE_PLAYER_JOIN, packet -> world.handlePlayerJoin((SPacketCreatePlayer) packet));
        connection.registerHandlerSync(ConnectionOption.HANDLE_PLAYER_LEAVE, packet -> world.handlePlayerLeave((SPacketRemovePlayer) packet));
        // TODO: Implement a join world timeout if you desire.

By default, all options are disabled, meaning Lunar will not handle anything itself.

With connection.enableOption() we can tell Lunar what to do for us. Above, we tell it to handle position and velocity updates as-well as basic authentication and box2d body forces.

Next, we add handlers for handling our custom world logic. With ConnectionOption.HANDLE_JOIN_WORLD this will fire whenever the local player joins a new world. We also add handling for players joining and leaving our world.

All that is left to do is tell the remote server we want to join a world.

player.getConnection().joinWorld("worldName", player.getName());

Step 6: Make your remote server.

Check out the basic example here

Step 7: All the rest

The rest is up to you on how to handle rendering your game. I'll post all of my example here so you can get an idea of how my game works.

    /**
     * Initializes camera and viewport. Default scaling is 16.
     */
    private void initializeCameraAndViewport() {
        // Initialize our graphics for drawing.
        camera = new OrthographicCamera();
        camera.setToOrtho(false, Gdx.graphics.getWidth() / ((1 / 16.0f) / 2.0f), Gdx.graphics.getHeight() / ((1 / 16.0f) / 2.0f));
        viewport = new ExtendViewport(Gdx.graphics.getWidth() / (1 / 16.0f), Gdx.graphics.getHeight() / (1 / 16.0f));

        // Set the initial camera position
        camera.position.set(0.0f, 0.0f, 0.0f);
        camera.update();
    }

    @Override
    public void render() {
        ScreenUtils.clear(69 / 255f, 8f / 255f, 163f / 255, 0.5f);

        if (ready) {
            final float delta = Gdx.graphics.getDeltaTime();

            // update our camera
            camera.position.set(player.getInterpolated().x, player.getInterpolated().y, 0f);
            camera.update();

            // update the world.
            world.update(delta);

            // begin batch
            batch.setProjectionMatrix(camera.combined);
            batch.begin();

            // render our player
            player.render(batch, delta);
            // render all network players
            for (LunarPlayerMP player : world.getPlayers().values()) {
                batch.draw(player.getRegion("player"), player.getX(), player.getY(),
                        player.getWidthScaled(), player.getHeightScaled());
            }

            batch.end();
            batch.begin();
            // debug information ---
            font.draw(batch, "L", player.getX() + 0.2f, player.getY() - 0.5f);
            for (LunarPlayerMP player : world.getPlayers().values()) {
                font.draw(batch, player.getEntityId() + "", player.getX() - 0.5f, player.getY() - 0.5f);
            }
            // ---
            batch.end();

        }
    }

    @Override
    public void resize(int width, int height) {
        viewport.update(width, height, false);
        camera.setToOrtho(false, width / 16f / 2f, height / 16f / 2f);
    }