Here we will create a simple multiplayer game using Cotta and LibGDX. The game is a networked 2D application where each player can move a square around using WASD. No shooting, no jumping, no nothing. Just moving for now, to not make this guide too overwhelming. Other features of the engine will be covered in later chapters.
The guide assumes some basic knowledge of Gradle, Kotlin and LibGDX.
The plan is roughly as follows:
The project called gdx-liftoff
is a good starting point. Just follow the
instructions in the project’s README.
Since we’re developing a multiplayer game, in addition to the default Desktop and Core modules, you should also check
And on the “Languages” tab check Kotlin. And generate and open in IDE(A).
Add jitpack.io
to your repositories section, if not yet:
repositories {
maven { url 'https://jitpack.io' }
}
Define the version of Cotta in the root gradle.properties
:
cottaVersion=0.1.2
Cotta uses KSP, so define versions of KSP and Kotlin as well:
kotlinVersion=1.9.24
kspVersion=1.0.20
Note: you may want to use different version of Kotlin, if you do - check KSP’s “Releases” page and look for a version that matches your version of Kotlin
Now add the dependency on KSP to the buildscript
section:
buildscript {
// ...
dependencies {
// ...
classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlinVersion-$kspVersion"
}
}
The submodules in the resulting project need the dependencies as follows:
cotta-server
cotta-core
and cotta-annotations-processor
cotta-gdx
Add the following:
dependencies {
...
implementation "com.mgtriffid.cotta:cotta-server:$cottaVersion"
}
Add the following:
dependencies {
...
implementation "com.mgtriffid.cotta:cotta-gdx:$cottaVersion"
}
Add the following:
apply {
plugin("com.google.devtools.ksp")
}
dependencies {
...
implementation "com.mgtriffid.cotta:cotta-core:$cottaVersion"
ksp "com.mgtriffid.cotta:cotta-annotations-processor:$cottaVersion"
}
All code in this section should be put into the shared module.
A game using Cotta needs to be defined in a specific way. It must be a class
that extends CottaGame
and is annotated with @Game
. Like this:
@Game
class ShowcaseGame : CottaGame {
override val config: CottaConfig = ShowcaseConfig()
override val inputProcessing: InputProcessing = ShowcaseInputProcessing()
override val playerInputKClass = ShowcasePlayerInput::class
override val playersHandler = ShowcasePlayersHandler()
override val systems: List<CottaSystem> = listOf()
}
You also need to define these classes ShowcaseConfig
, ShowcasePlayerInput
,
ShowcaseInputProcessing
, ShowcasePlayersHandler
.
ShowcaseConfig
contains configuration like tick length and network settings.
For now let’s just configure the tickLength
and leave everything else as
default:
class ShowcaseConfig : CottaConfig {
override val tickLength: Long = 33L // to tick roughly 30 times per second
}
Since we’re making player move around with WASD, one byte of information for
walking direction is enough for us. Note that we don’t put “W pressed” things
here, because that is related to control, not to actual game logic input. We
define ShowcasePlayerInput
in terms of actual game and not keyboard like this:
data class ShowcasePlayerInput(
val walkingDirection: Byte
) : PlayerInput
object WalkingDirections {
const val IDLE: Byte = 0
const val UP: Byte = 1
const val DOWN: Byte = 2
const val LEFT: Byte = 3
const val RIGHT: Byte = 4
}
For now, we will leave the remaining two classes empty. We will fill them later when components are ready:
class ShowcaseInputProcessing : InputProcessing {
override fun processPlayerInput(
playerId: PlayerId,
input: PlayerInput,
entities: Entities,
effectBus: EffectBus
) {
// will fill this later
}
}
class ShowcasePlayersHandler : PlayersHandler
Components are the data that entities have. Cotta requires them to reside in the
same package as the game class, or in a nested sub-package. So if you had a
package like com.example.game
, you can put components in the
com.example.game.components
package.
Components are defined as interfaces, then cotta-annotation-processor
uses
Google’s KSP to generate implementations.
Let’s define components for position and to mark an entity as controlled by a player:
interface PositionComponent : MutableComponent<PositionComponent> {
var x: Float
var y: Float
}
interface ControllableComponent : MutableComponent<ControllableComponent> {
var direction: Byte
val playerId: PlayerId
}
Run gradle clean build
now to ensure that it builds. KSP should generate the
implementation classes for these components. Using your IDE look for a file
PositionComponentImpl.kt
located in
shared/build/generated/ksp/main/kotlin/.../components/PositionComponentImpl.kt
.
If it’s not there - something went wrong, please double-check that you have
added KSP plugin and dependencies correctly.
Systems are the logic that runs on entities. We will define a system that moves
our entities that have both PositionComponent
and ControllableComponent
:
class MovementSystem : EntityProcessingSystem {
override fun process(e: Entity, ctx: EntityProcessingContext) {
if (e.hasComponent(ControllableComponent::class) &&
e.hasComponent(PositionComponent::class)
) {
val controllable = e.getComponent(ControllableComponent::class)
val position = e.getComponent(PositionComponent::class)
val velX = when (controllable.direction) {
WalkingDirections.LEFT -> -300f
WalkingDirections.RIGHT -> 300f
else -> 0f
}
val velY = when (controllable.direction) {
WalkingDirections.UP -> 300f
WalkingDirections.DOWN -> -300f
else -> 0f
}
// ctx.clock().delta() is frame time in seconds basically:
position.x += velX * ctx.clock().delta()
position.y += velY * ctx.clock().delta()
}
}
}
As of version 0.1.2
, all EntityProcessingSystem
s are called for all
Entities, so we need to check for components existence here to filter out those
that don’t have to be moved (if we have any). In the future this API will likely
change.
Now add this system to the list of systems in the ShowcaseGame
class:
@Game
class ShowcaseGame : CottaGame {
// ...
override val systems: List<CottaSystem> = listOf(MovementSystem())
}
PlayersHandler
to handle players joining/leavingThe ShowcasePlayersHandler
will create an Entity for each player that joins
and remove it once player leaves:
class ShowcasePlayersHandler : PlayersHandler {
override fun onEnterGame(playerId: PlayerId, entities: Entities) {
// ownedBy matters for prediction:
entities.create(ownedBy = Entity.OwnedBy.Player(playerId)).apply {
// createControllableComponent function is generated by KSP during build
addComponent(createControllableComponent(0, playerId))
addComponent(createPositionComponent(120f, 120f))
}
}
override fun onLeaveGame(playerId: PlayerId, entities: Entities) {
val entityId = entities.all().find { it.ownedBy == Entity.OwnedBy.Player(playerId) }?.id
if (entityId != null) {
entities.remove(entityId)
}
}
}
We saw that we have that ControllableComponent
that has direction
field. We
know it is used in the MovementSystem
. But we haven’t yet wrote a piece of
code that would deliver direction
from ShowcasePlayerInput
to
ControllableComponent
. Now we do so:
class ShowcaseInputProcessing : InputProcessing {
override fun processPlayerInput(
playerId: PlayerId,
input: PlayerInput,
entities: Entities,
effectBus: EffectBus
) {
val character = getCharacter(entities, playerId) ?: return
val controllable = character.getComponent(ControllableComponent::class)
controllable.direction = (input as ShowcasePlayerInput).walkingDirection
}
private fun getCharacter(entities: Entities, playerId: PlayerId): Entity? {
return entities.all().find { it.ownedBy == Entity.OwnedBy.Player(playerId) }
}
}
Shared module is ready now. Let’s move to the core module.
ShowcasePlayerInput
For this we will implement a special interface CottaGdxClientInput
like this:
class ShowcaseCottaClientGdxInput : CottaClientGdxInput {
private val storage = Storage()
// Supposed to be called as frequently as possible - in render() calls.
// Since render() may be called more often than game simulation ticks,
// we accumulate inputs to a special Storage.
override fun accumulate() {
with(storage) {
upPressed = upPressed || Gdx.input.isKeyPressed(Input.Keys.W)
downPressed = downPressed || Gdx.input.isKeyPressed(Input.Keys.S)
leftPressed = leftPressed || Gdx.input.isKeyPressed(Input.Keys.A)
rightPressed = rightPressed || Gdx.input.isKeyPressed(Input.Keys.D)
}
}
override fun input(): ShowcasePlayerInput {
return ShowcasePlayerInput(
when {
storage.upPressed && !storage.downPressed -> WalkingDirections.UP
!storage.upPressed && storage.downPressed -> WalkingDirections.DOWN
storage.leftPressed && !storage.rightPressed -> WalkingDirections.LEFT
!storage.leftPressed && storage.rightPressed -> WalkingDirections.RIGHT
else -> WalkingDirections.IDLE
},
).also { clear() }
}
private fun clear() {
storage.upPressed = false
storage.downPressed = false
storage.leftPressed = false
storage.rightPressed = false
}
private class Storage {
var upPressed: Boolean = false
var downPressed: Boolean = false
var leftPressed: Boolean = false
var rightPressed: Boolean = false
}
}
This was straightforward.
Under the kotlin
folder of the core
module create a package and there add
a file DrawableComponents.kt
fun getDrawableComponentClasses(): List<KClass<Component<*>>> = listOf(
position as KClass<Component<*>>
)
val position = PositionComponent::class
Add the following lines to the create()
method of Main
class:
@Override
public void create() {
batch = new SpriteBatch();
image = new Texture("libgdx.png");
character = new Texture("square.png");
CottaGame game = new ShowcaseGame();
gdxAdapter = new CottaGdxAdapter(game, new ShowcaseCottaClientGdxInput());
gdxAdapter.initialize();
}
This code creates an instance of your game, passes it to the CottaGdxAdapter
which will actually run the game, along with the ShowcaseCottaClientGdxInput
.
And create some square.png
sprite, like 32x32, and put it into the assets
folder of the project.
Next, change the render()
method as follows:
@Override
public void render() {
Gdx.gl.glClearColor(0.15f, 0.15f, 0.2f, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
UpdateResult result = gdxAdapter.invoke(); // attempt to tick the game
if (result instanceof UpdateResult.Running) {
draw((UpdateResult.Running) result);
} else {
batch.begin();
batch.draw(image, 140, 210);
batch.end();
}
}
private final List<KClass<Component<?>>> drawableComponents = DrawableComponentsKt.getDrawableComponentClasses();
private void draw(UpdateResult.Running result) {
DrawableState state = gdxAdapter.getDrawableState(result.getAlphas(), drawableComponents);
batch.begin();
if (state instanceof DrawableState.Ready) {
for (Entity entity : ((DrawableState.Ready) state).getEntities()) {
// here we actually draw the entities as squares
PositionComponent positionComponent = entity.getComponent(DrawableComponentsKt.getPosition());
batch.draw(character, positionComponent.getX(), positionComponent.getY());
}
}
batch.end();
}
So the whole Main
class should look like this:
public class Main extends ApplicationAdapter {
private SpriteBatch batch;
private Texture image;
private Texture character;
private CottaGdxAdapter gdxAdapter;
private final List<KClass<Component<?>>> drawableComponents = DrawableComponentsKt.getDrawableComponentClasses();
@Override
public void create() {
batch = new SpriteBatch();
image = new Texture("libgdx.png");
character = new Texture("square.png");
CottaGame game = new ShowcaseGame();
gdxAdapter = new CottaGdxAdapter(game, new ShowcaseCottaClientGdxInput());
gdxAdapter.initialize();
}
@Override
public void render() {
Gdx.gl.glClearColor(0.15f, 0.15f, 0.2f, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
UpdateResult result = gdxAdapter.invoke();
if (result instanceof UpdateResult.Running) {
draw((UpdateResult.Running) result);
} else {
batch.begin();
batch.draw(image, 140, 210);
batch.end();
}
}
private void draw(UpdateResult.Running result) {
DrawableState state = gdxAdapter.getDrawableState(result.getAlphas(), drawableComponents);
batch.begin();
if (state instanceof DrawableState.Ready) {
for (Entity entity : ((DrawableState.Ready) state).getEntities()) {
PositionComponent positionComponent = entity.getComponent(DrawableComponentsKt.getPosition());
batch.draw(character, positionComponent.getX(), positionComponent.getY());
}
}
batch.end();
}
@Override
public void dispose() {
batch.dispose();
image.dispose();
character.dispose();
}
}
This is short.
Create a ShowcaseServer
Kotlin class:
class ShowcaseServer {
fun start() {
CottaServerFactory()
.create(ShowcaseGame())
.initializeInstances()
}
}
and make the ServerLauncher
run it:
public class ServerLauncher {
public static void main(String[] args) {
new ShowcaseServer().start();
}
}
Once again do gradle clean build
to ensure that everything is fine.
Then run gradle server:run
to start the Server.
Then wait for couple seconds and run gradle lwjgl3:run
to start the Client.
You should see a square that you can move around with WASD keys:
When you move the square around, you may notice that it doesn’t move smoothly,
it jitters visibly. And also the response is not instant. This is because the
square is moved only when our input travels to the server and then travels back,
and only then client simulation takes this input into account. If you don’t see
these problems easily, then try to increase the tickLength
to like 100L
.
Annotate the MovementSystem
with @Predicted
:
@Predicted
class MovementSystem : EntityProcessingSystem {
// ...
}
Annotate both fields of the PositionComponent
as @Interpolated
:
interface PositionComponent : MutableComponent<PositionComponent> {
@Interpolated var x: Float
@Interpolated var y: Float
}
Clean, build, launch server, launch client. See the difference. Now the response time is within the tick length, and the square moves smoothly.
These and other features will be covered in further guides.
All code from here is available here https://github.com/mgtriffid/cotta-showcase , revision 78db8dc2f48f25ddcfaf10d5ecf10ffe9050b378
Next let’s get to know Effects.