-
Notifications
You must be signed in to change notification settings - Fork 1
Quick Start Guide
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
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);
}
}
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
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.
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.
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());
Check out the basic example here
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);
}
Getting Started
Server
Networking
Entities
Worlds
Instances