Skip to content
Vrekt edited this page Oct 1, 2021 · 36 revisions

LunarGdx

Getting Started

How Lunar Works

Lunar works by keeping track of players in their own entity classes. Those players are apart of worlds which keeps track of all players. Each player is responsible for updating their own position. Each player also will handle other player information and pass it along to the world.

Here you can view a few pre-made examples.

Components

Entities

Creating your own Player entity

Lunar comes with a entity that is already defined for you LunarPlayer. But if you wish to have more advanced behaviour or customization you can do this by extending LunarEntityPlayer or LunarPlayer.

Each player entity should extend LunarPlayer or LunarEntityPlayer.

public class Player extends LunarPlayer {

    public Player() {
        super(scaling, width, height, rotation);
    }

}

From here there are a few options you could configure.

  • player.setPositionSendRate(ms) defines how often to send position updates to the server.
  • player.setVelocitySendRate(ms) defines how often to send velocity updates to the server. setMoveSpeed(f)` is actually that, set your move speed.

It's as simple as that.

Player movement

By default LunarPlayer provides a movement system using WASD. If you wish to not use this you MUST override the update() method.

Player movement is provided via the move(x, y, rotation) method or by setting the velocity yourself. Players use Box2d by default, meaning impulses, forces or velocity is used instead of directly setting the position.

    public void doSomething() {
        this.player.move(1.0f, 0.0f, Rotation.FACING_LEFT);
    }

The snippet above applies a X velocity of 1.0 and sets the players rotation to left. If using the provided snippet, to stop movement you would simply apply a velocity of 0.0. this.player.move(0.0, 0.0f, Rotation.FACING_LEFT);

Otherwise, you could poll for input every update() and apply velocity yourself.

        if (Gdx.input.isKeyPressed(Input.Keys.A)) {
            velocity.set(-moveSpeed, 0f);
            rotation = Rotation.FACING_LEFT;
        } else if (Gdx.input.isKeyPressed(Input.Keys.D)) {
            velocity.set(moveSpeed, 0f);
            rotation = Rotation.FACING_RIGHT;
        }
Player movement protocol

By default LunarPlayer will send position and velocity updates every X interval set by you (150 ms, 100 ms default). If you want to do this manually, you MUST override the update() method.

To send position and velocity updates simply:

// send my position
connection.send(new CPacketPosition(connection.alloc(), currentRotation.ordinal(), currentX, currentY));
// send my velocity
connection.send(new CPacketVelocity(connection.alloc(), velocity.x, velocity.y,  currentRotation.ordinal()));

Player rotation is sent over the network by using the ordinal index of the enum.

Player Collision

Lunar includes a basic collision handler for players. To disable or enable this state simply:

this.player.setIgnoreOtherPlayerCollision(boolean)

If true, you would want to add a collision handler to your Box2d world using PlayerCollisionListener.

// for example
final World world = new World(Vector2.Zero, true);
world.setContactListener(new PlayerCollisionListener());
Setting players username

Once connected to the remote server, you can call player.setName() and this will set the username and send it to the server.

You can also connection.sendSetUsername(username).

Spawning entities in a world.

The LunarEntity class provides public abstract void spawnEntityInWorld(LunarWorld world, float x, float y); method for spawning the entity in a Box2d world.

Its up to the extending class to implement this. The LunarEntityPlayer provides a default implementation which:

  • Sets the current, previous and interpolated position to the X, Y provided.
  • Creates a dynamic body type with fixed rotation.
  • Sets the shape of the entity as a Polygon box (width / 2f, height / 2f)
  • Sets the density of the entity as 1.0f
  • Sets user data as LunarEntityPlayer

You can change this behavior simply by overriding spawnEntityInWorld.

Lunar worlds have no concept of a local player, only networked players.

Player rendering

Player rendering works by using a TextureAtlas of animations and idle states. The default player rendering only supports movement animations and idle states and nothing else.

You can create your own renderer with all the special features you desire by extending LunarPlayerRenderer. If you choose to do so ensure you myRenderer.load() and then player.initializePlayerRendererWith(myRenderer);`

Otherwise using the default renderer is simple You have 2 ways of defining the TextureAtlas for use in the renderer.

Walking animations can be defined as walking_up, walking_down, walking_left, walking right. They each must have an index of 1 and 2. (Indicating two separate animations for each player leg).

Idle animations can defined as the same except for adding "_idle" at the end. For example: walking_up_idle. These don't need an index.

With that, everything should work by default, simply plug it in:

final TextureAtlas atlas = new TexureAtlas(...)
player.initializePlayerRendererAndLoad(atlas, true);

Otherwise, if you wish to use other naming conventions or have different names you have an option. Start by defining a map of walking and idle animations.

final Map<Rotation, String> walkingAnimations = new HashMap<>();
final Map<Rotation, String> idleAnimations = new HashMap<>();

Next, plug in the names of each animation, for example:

walkingAnimations.put(Rotation.FACING_UP, "character_up");
walkingAnimations.put(Rotation.FACING_DOWN, "character_down");
walkingAnimations.put(Rotation.FACING_LEFT, "character_left");
walkingAnimations.put(Rotation.FACING_RIGHT, "character_right");

idleAnimations.put(Rotation.FACING_UP, "character_up_idle");
idleAnimations.put(Rotation.FACING_DOWN, "character_down_idle");
idleAnimations.put(Rotation.FACING_LEFT, "character_left_idle");
idleAnimations.put(Rotation.FACING_RIGHT, "character_right_idle");

With that you could create a new instance of DefaultPlayerRenderer (or your own implementation) and plug in the values.

        // Supply the atlas, default rotation, walking and idle art,
        // width and height of the player and if position should be offset
        // to fit inside box2d collision.
        return new DefaultPlayerRenderer(
                atlas,
                Rotation.FACING_UP,
                walkingAnimations,
                idleAnimations,
                width * scaling,
                height * scaling,
                true);

Finally, to provide this to the player you must load first and then call the method.

renderer.load();
player.initializePlayerRendererWith(renderer);
What is "offsetBox2d"

By default, when rendering the player their position will not be within the bounds of their Box2d body. This causes issues with collision and other things. This is fixed by using offsetBox2d, it works by subtracting the width from X and height from Y and dividing by 2.

If this causes issues for you, set offsetBox2d to false when initializing renderers.

Networked Entities or Players

Lunar by default provides a default implementation of players that are networked. They are LunarNetworkPlayer and LunarNetworkEntityPlayer.

LunarNetworkEntityPlayer handles interpolating position, setting velocity and rendering. You can change the distance before lunar will reset players position caused by desync with:

networkPlayer.setInterpolateDesyncDistance(5.0f)

The default is 3.0

Worlds

Lunar provides a world which could be a level, instance or... a world. Worlds manage players within them and syncs objects and entities. They could also manage updating Box2d worlds, the local player, and all network players.

There are a few configuration options within worlds:

  • setStepTime(f) Allows you to set the step time for Box2d worlds.
  • setMaxFrameTime(f) Allows you to set the max time a frame is allowed to take.
  • setVelocityIterations(i) Allows you to set velocity iterations in the Box2d world.
  • setPositionIterations(i) Allows you to set position iterations in the Box2d world.
  • setHandlePhysics(bool) Allows you to set whether or not the world will update the Box2d world.
  • setUpdatePlayer(bool) If true, the world will handle updating your local player.
  • setUpdateNetworkPlayers(bool) If true, the world will handle updating all network players.
Remove or add players in a world
world.setPlayerInWorld(player)
world.removePlayerFromWorld(player)
world.removePlayerFromWorld(entityId)
Set position and velocity of networked players.
world.updatePlayerPosition(entityId, x, y, rotation)
world.updatePlayerVelocity(entityId velX, velY, rotation)
Rendering network players in a world.
@Override
public void renderWorld(SpriteBatch batch, float delta) {
    for (LunarNetworkEntityPlayer value : players.values()) value.render(batch, delta);
}
Custom world behavior

You could define a custom world by extending LunarWorld.

Applying forces to a player in worlds

You can apply forces to yourself that will be sent to all other players by calling applyForceToPlayerNetwork in World.

this.player.getWorldIn().applyForceToPlayerNetwork(player.getConnection(), fx, fy, point.x, point.y, true);

You can also apply forces to others and have that sync as-well.

this.player.getWorldIn().applyForceToOtherPlayerNetwork(somePlayer, player.getConnection(), fx, fy, px, py, true);

Protocol and Networking

All protocol handling is done in the Protocol module with the class LunarProtocol. You must initialize the protocol before using it in the server or client. In most cases this is already done for you, however you can do it manually by calling LunarProtocol.initializeProtocol()

Registering custom packets.

You can register packets quickly by calling registerPacket method in AbstractConnection.

// register a unique custom packet.
this.connection.registerPacket(99, MyCustomPacket::new, packet -> {
    // Do something with the packet.
});

Otherwise, there are methods within LunarProtocol for a more low level approach.

You can also change default handlers for lunar packets within LunarProtocol.

LunarProtocol.changeDefaultServerPacketHandlerFor(SPacketPlayerPosition.PID, (in, handler)
        -> this.connection.handleCustomPacket(new MyCustomPositionPacketServer(in)));

Creating a server

TODO

        final LunarNettyServer server = new LunarNettyServer("localhost", 6969);
        server.bind().join();

        final ImplLunarServer s = new ImplLunarServer();
        s.start();