Skip to content

Commit 80b25ac

Browse files
committed
feat: MultiplayerService now provides ping (round-trip time) information for a given connection, closes #877
1 parent d44c647 commit 80b25ac

File tree

3 files changed

+70
-6
lines changed

3 files changed

+70
-6
lines changed

fxgl-samples/src/main/java/sandbox/net/MultiplayerSample.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ protected void initGame() {
138138
server.setOnConnected(conn -> {
139139
connection = conn;
140140

141+
getMPService().registerConnection(conn);
142+
141143
getExecutor().startAsyncFX(() -> {
142144
player1 = spawn("player1", 150, 150);
143145
getMPService().spawn(connection, player1, "player1");
@@ -149,6 +151,11 @@ protected void initGame() {
149151
getMPService().addPropertyReplicationSender(conn, getWorldProperties());
150152

151153
getMPService().addEventReplicationSender(conn, clientBus);
154+
155+
var textPing = getUIFactoryService().newText("", Color.BLUE, 14.0);
156+
textPing.textProperty().bind(getMPService().pingProperty(conn).divide(1000000).asString("Ping: %.0f ms"));
157+
158+
addUINode(textPing, 50, 200);
152159
});
153160
});
154161

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

165-
166172
var client = getNetService().newTCPClient("localhost", 55555);
167173
client.setOnConnected(conn -> {
174+
getMPService().registerConnection(conn);
175+
168176
getMPService().addEntityReplicationReceiver(conn, getGameWorld());
169177
getMPService().addInputReplicationSender(conn, getInput());
170178
getMPService().addPropertyReplicationReceiver(conn, getWorldProperties());

fxgl/src/main/kotlin/com/almasb/fxgl/multiplayer/MultiplayerService.kt

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
package com.almasb.fxgl.multiplayer
88

99
import com.almasb.fxgl.core.EngineService
10-
import com.almasb.fxgl.core.collection.PropertyChangeListener
10+
import com.almasb.fxgl.core.collection.MovingAverageQueue
1111
import com.almasb.fxgl.core.collection.PropertyMap
1212
import com.almasb.fxgl.core.collection.PropertyMapChangeListener
1313
import com.almasb.fxgl.core.serialization.Bundle
@@ -18,6 +18,8 @@ import com.almasb.fxgl.event.EventBus
1818
import com.almasb.fxgl.input.*
1919
import com.almasb.fxgl.logging.Logger
2020
import com.almasb.fxgl.net.Connection
21+
import javafx.beans.property.ReadOnlyDoubleProperty
22+
import javafx.beans.property.ReadOnlyDoubleWrapper
2123

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

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

35+
fun registerConnection(connection: Connection<Bundle>) {
36+
val data = ConnectionData(connection)
37+
setUpNewConnection(data)
38+
39+
replicatedEntitiesMap[connection] = data
40+
}
41+
42+
private fun setUpNewConnection(data: ConnectionData) {
43+
// register event handler for the given connection
44+
// TODO: how to clean up when the connection dies
45+
addEventReplicationReceiver(data.connection, data.eventBus)
46+
47+
data.eventBus.addEventHandler(ReplicationEvent.PING) { ping ->
48+
val timeRecv = System.nanoTime()
49+
fire(data.connection, PongReplicationEvent(ping.timeSent, timeRecv))
50+
}
51+
52+
data.eventBus.addEventHandler(ReplicationEvent.PONG) { pong ->
53+
val timeNow = System.nanoTime()
54+
val roundTripTime = timeNow - pong.timeSent
55+
56+
data.pingBuffer.put(roundTripTime.toDouble())
57+
data.ping.value = data.pingBuffer.average
58+
}
59+
}
60+
3361
override fun onGameUpdate(tpf: Double) {
3462
if (replicatedEntitiesMap.isEmpty())
3563
return
3664

65+
val now = System.nanoTime()
66+
3767
// TODO: can (should) we move this to NetworkComponent to act on a per entity basis ...
3868
replicatedEntitiesMap.forEach { conn, data ->
69+
fire(conn, PingReplicationEvent(now))
70+
3971
if (data.entities.isNotEmpty()) {
4072
updateReplicatedEntities(conn, data.entities)
4173
}
4274
}
4375
}
4476

77+
/**
78+
* @return round-trip time from this endpoint to given [connection]
79+
*/
80+
fun pingProperty(connection: Connection<Bundle>): ReadOnlyDoubleProperty {
81+
// TODO: if no connection in map
82+
return replicatedEntitiesMap[connection]!!.ping.readOnlyProperty
83+
}
84+
4585
private fun updateReplicatedEntities(connection: Connection<Bundle>, entities: MutableList<Entity>) {
4686
val events = arrayListOf<ReplicationEvent>()
4787

@@ -73,9 +113,9 @@ class MultiplayerService : EngineService() {
73113

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

76-
val data = replicatedEntitiesMap.getOrDefault(connection, ConnectionData())
116+
// TODO: if not available
117+
val data = replicatedEntitiesMap[connection]!!
77118
data.entities += entity
78-
replicatedEntitiesMap[connection] = data
79119

80120
fire(connection, event)
81121
}
@@ -237,7 +277,11 @@ class MultiplayerService : EngineService() {
237277
}
238278
}
239279

240-
private class ConnectionData {
280+
private class ConnectionData(val connection: Connection<Bundle>) {
241281
val entities = ArrayList<Entity>()
282+
val eventBus = EventBus().also { it.isLoggingEnabled = false }
283+
284+
val pingBuffer = MovingAverageQueue(1000)
285+
val ping = ReadOnlyDoubleWrapper()
242286
}
243287
}

fxgl/src/main/kotlin/com/almasb/fxgl/multiplayer/ReplicationEvent.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ abstract class ReplicationEvent(eventType: EventType<out ReplicationEvent>) : Ev
3030

3131
@JvmField val PROPERTY_UPDATE = EventType(ANY, "PROPERTY_UPDATE")
3232
@JvmField val PROPERTY_REMOVE = EventType(ANY, "PROPERTY_REMOVE")
33+
34+
@JvmField val PING = EventType<PingReplicationEvent>(ANY, "PING")
35+
@JvmField val PONG = EventType<PongReplicationEvent>(ANY, "PONG")
3336
}
3437
}
3538

@@ -70,4 +73,13 @@ class PropertyUpdateReplicationEvent(
7073

7174
class PropertyRemoveReplicationEvent(
7275
val propertyName: String
73-
) : ReplicationEvent(PROPERTY_REMOVE)
76+
) : ReplicationEvent(PROPERTY_REMOVE)
77+
78+
class PingReplicationEvent(
79+
val timeSent: Long
80+
) : ReplicationEvent(PING)
81+
82+
class PongReplicationEvent(
83+
val timeSent: Long,
84+
val timeReceived: Long
85+
) : ReplicationEvent(PONG)

0 commit comments

Comments
 (0)