Skip to content

Commit

Permalink
feat: MultiplayerService now provides ping (round-trip time) informat…
Browse files Browse the repository at this point in the history
…ion for a given connection, closes #877
  • Loading branch information
AlmasB committed May 6, 2023
1 parent d44c647 commit 80b25ac
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 6 deletions.
10 changes: 9 additions & 1 deletion fxgl-samples/src/main/java/sandbox/net/MultiplayerSample.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ protected void initGame() {
server.setOnConnected(conn -> {
connection = conn;

getMPService().registerConnection(conn);

getExecutor().startAsyncFX(() -> {
player1 = spawn("player1", 150, 150);
getMPService().spawn(connection, player1, "player1");
Expand All @@ -149,6 +151,11 @@ protected void initGame() {
getMPService().addPropertyReplicationSender(conn, getWorldProperties());

getMPService().addEventReplicationSender(conn, clientBus);

var textPing = getUIFactoryService().newText("", Color.BLUE, 14.0);
textPing.textProperty().bind(getMPService().pingProperty(conn).divide(1000000).asString("Ping: %.0f ms"));

addUINode(textPing, 50, 200);
});
});

Expand All @@ -162,9 +169,10 @@ protected void initGame() {
text.setFont(Font.font(26.0));
}, Duration.seconds(5));


var client = getNetService().newTCPClient("localhost", 55555);
client.setOnConnected(conn -> {
getMPService().registerConnection(conn);

getMPService().addEntityReplicationReceiver(conn, getGameWorld());
getMPService().addInputReplicationSender(conn, getInput());
getMPService().addPropertyReplicationReceiver(conn, getWorldProperties());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
package com.almasb.fxgl.multiplayer

import com.almasb.fxgl.core.EngineService
import com.almasb.fxgl.core.collection.PropertyChangeListener
import com.almasb.fxgl.core.collection.MovingAverageQueue
import com.almasb.fxgl.core.collection.PropertyMap
import com.almasb.fxgl.core.collection.PropertyMapChangeListener
import com.almasb.fxgl.core.serialization.Bundle
Expand All @@ -18,6 +18,8 @@ import com.almasb.fxgl.event.EventBus
import com.almasb.fxgl.input.*
import com.almasb.fxgl.logging.Logger
import com.almasb.fxgl.net.Connection
import javafx.beans.property.ReadOnlyDoubleProperty
import javafx.beans.property.ReadOnlyDoubleWrapper

/**
* TODO: symmetric remove API, e.g. removeReplicationSender()
Expand All @@ -30,18 +32,56 @@ class MultiplayerService : EngineService() {

private val replicatedEntitiesMap = hashMapOf<Connection<Bundle>, ConnectionData>()

fun registerConnection(connection: Connection<Bundle>) {
val data = ConnectionData(connection)
setUpNewConnection(data)

replicatedEntitiesMap[connection] = data
}

private fun setUpNewConnection(data: ConnectionData) {
// register event handler for the given connection
// TODO: how to clean up when the connection dies
addEventReplicationReceiver(data.connection, data.eventBus)

data.eventBus.addEventHandler(ReplicationEvent.PING) { ping ->
val timeRecv = System.nanoTime()
fire(data.connection, PongReplicationEvent(ping.timeSent, timeRecv))
}

data.eventBus.addEventHandler(ReplicationEvent.PONG) { pong ->
val timeNow = System.nanoTime()
val roundTripTime = timeNow - pong.timeSent

data.pingBuffer.put(roundTripTime.toDouble())
data.ping.value = data.pingBuffer.average
}
}

override fun onGameUpdate(tpf: Double) {
if (replicatedEntitiesMap.isEmpty())
return

val now = System.nanoTime()

// TODO: can (should) we move this to NetworkComponent to act on a per entity basis ...
replicatedEntitiesMap.forEach { conn, data ->
fire(conn, PingReplicationEvent(now))

if (data.entities.isNotEmpty()) {
updateReplicatedEntities(conn, data.entities)
}
}
}

/**
* @return round-trip time from this endpoint to given [connection]
*/
fun pingProperty(connection: Connection<Bundle>): ReadOnlyDoubleProperty {
// TODO: if no connection in map
return replicatedEntitiesMap[connection]!!.ping.readOnlyProperty
}

private fun updateReplicatedEntities(connection: Connection<Bundle>, entities: MutableList<Entity>) {
val events = arrayListOf<ReplicationEvent>()

Expand Down Expand Up @@ -73,9 +113,9 @@ class MultiplayerService : EngineService() {

val event = EntitySpawnEvent(networkComponent.id, entityName, entity.x, entity.y, entity.z)

val data = replicatedEntitiesMap.getOrDefault(connection, ConnectionData())
// TODO: if not available
val data = replicatedEntitiesMap[connection]!!
data.entities += entity
replicatedEntitiesMap[connection] = data

fire(connection, event)
}
Expand Down Expand Up @@ -237,7 +277,11 @@ class MultiplayerService : EngineService() {
}
}

private class ConnectionData {
private class ConnectionData(val connection: Connection<Bundle>) {
val entities = ArrayList<Entity>()
val eventBus = EventBus().also { it.isLoggingEnabled = false }

val pingBuffer = MovingAverageQueue(1000)
val ping = ReadOnlyDoubleWrapper()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ abstract class ReplicationEvent(eventType: EventType<out ReplicationEvent>) : Ev

@JvmField val PROPERTY_UPDATE = EventType(ANY, "PROPERTY_UPDATE")
@JvmField val PROPERTY_REMOVE = EventType(ANY, "PROPERTY_REMOVE")

@JvmField val PING = EventType<PingReplicationEvent>(ANY, "PING")
@JvmField val PONG = EventType<PongReplicationEvent>(ANY, "PONG")
}
}

Expand Down Expand Up @@ -70,4 +73,13 @@ class PropertyUpdateReplicationEvent(

class PropertyRemoveReplicationEvent(
val propertyName: String
) : ReplicationEvent(PROPERTY_REMOVE)
) : ReplicationEvent(PROPERTY_REMOVE)

class PingReplicationEvent(
val timeSent: Long
) : ReplicationEvent(PING)

class PongReplicationEvent(
val timeSent: Long,
val timeReceived: Long
) : ReplicationEvent(PONG)

0 comments on commit 80b25ac

Please sign in to comment.