Skip to content

Commit 4c2b422

Browse files
feat: AsyncService for parallel EngineService processing
Co-authored-by: Jean-René Lavoie <>
1 parent bb60463 commit 4c2b422

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* FXGL - JavaFX Game Library. The MIT License (MIT).
3+
* Copyright (c) AlmasB ([email protected]).
4+
* See LICENSE for details.
5+
*/
6+
7+
package com.almasb.fxgl.core
8+
9+
import java.util.concurrent.CompletableFuture
10+
11+
/**
12+
*
13+
* @author Jean-Rene Lavoie ([email protected])
14+
*/
15+
abstract class AsyncService<T> : EngineService() {
16+
17+
private var asyncTask: CompletableFuture<T>? = null
18+
19+
/**
20+
* Call the async game update. On next game update, wait for the task to be completed (if not already) and
21+
* call onPostGameUpdateAsync to allow JavaFX thread dependent task handling (e.g. updating the Nodes)
22+
*/
23+
override fun onGameUpdate(tpf: Double) {
24+
asyncTask?.let { onPostGameUpdateAsync(it.get()) }
25+
asyncTask = CompletableFuture.supplyAsync(){ onGameUpdateAsync(tpf) } // Process until next onGameUpdate
26+
}
27+
28+
/**
29+
* Async game update processing method.
30+
* Warning: This will not run on the main JavaFX thread. This means that any changes done on the Nodes will cause
31+
* an exception.
32+
*/
33+
abstract fun onGameUpdateAsync(tpf: Double): T
34+
35+
/**
36+
* Async processing Callback. This method is called on next onGameUpdate allowing synchronization between this
37+
* Service async processing and the main JavaFX thread.
38+
*/
39+
open fun onPostGameUpdateAsync(result: T) { }
40+
41+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* FXGL - JavaFX Game Library. The MIT License (MIT).
3+
* Copyright (c) AlmasB ([email protected]).
4+
* See LICENSE for details.
5+
*/
6+
@file:Suppress("JAVA_MODULE_DOES_NOT_DEPEND_ON_MODULE")
7+
package com.almasb.fxgl.core
8+
9+
import org.hamcrest.MatcherAssert.assertThat
10+
import org.hamcrest.Matchers
11+
import org.hamcrest.Matchers.`is`
12+
import org.hamcrest.Matchers.greaterThan
13+
import org.hamcrest.Matchers.lessThan
14+
import org.hamcrest.Matchers.both
15+
import org.junit.jupiter.api.Assertions.assertEquals
16+
import org.junit.jupiter.api.Test
17+
import kotlin.system.measureTimeMillis
18+
19+
/**
20+
*
21+
* @author Jean-Rene Lavoie ([email protected])
22+
*/
23+
class AsyncServiceTest {
24+
25+
@Test
26+
fun `Async Service with Unit (Kotlin Void)`() {
27+
val service = object : AsyncService<Unit>() {
28+
override fun onGameUpdateAsync(tpf: Double) {
29+
Thread.sleep(100) // Processing takes more time than a normal tick
30+
}
31+
}
32+
33+
// On first call, it'll launch the async process and continue the game loop without affecting the tick
34+
// If it takes less than 5 millis, it's running async
35+
assertThat(measureTimeMillis { service.onGameUpdate(1.0) }.toDouble(), lessThan(7.0))
36+
37+
// On the second call, it must wait until the first call is resolved before calling it again (to prevent major desync)
38+
// We expect it to take more than 80 millis
39+
assertThat(measureTimeMillis { service.onGameUpdate(1.0) }.toDouble(), greaterThan(80.0))
40+
}
41+
42+
@Test
43+
fun `Async Service with T (String)`() {
44+
var postUpdateValue = ""
45+
val service = object : AsyncService<String>() {
46+
override fun onGameUpdateAsync(tpf: Double): String {
47+
return "Done"
48+
}
49+
50+
override fun onPostGameUpdateAsync(result: String) {
51+
postUpdateValue = result
52+
}
53+
}
54+
55+
// On first call, we don't have the postUpdateValue yet
56+
service.onGameUpdate(1.0)
57+
assertEquals(postUpdateValue, "")
58+
59+
// On second update, we updated the postUpdateValue
60+
service.onGameUpdate(1.0)
61+
assertEquals(postUpdateValue, "Done")
62+
}
63+
64+
@Test
65+
fun `Async Service parallel`() {
66+
val services = listOf(
67+
object : AsyncService<Unit>() {
68+
override fun onGameUpdateAsync(tpf: Double) {
69+
Thread.sleep(100) // Processing takes more time than a normal tick
70+
}
71+
},
72+
object : AsyncService<Unit>() {
73+
override fun onGameUpdateAsync(tpf: Double) {
74+
Thread.sleep(100) // Processing takes more time than a normal tick
75+
}
76+
},
77+
object : AsyncService<Unit>() {
78+
override fun onGameUpdateAsync(tpf: Double) {
79+
Thread.sleep(100) // Processing takes more time than a normal tick
80+
}
81+
}
82+
)
83+
84+
// 3 services started in parallel without additional latency
85+
assertThat(measureTimeMillis {
86+
services.forEach { service ->
87+
service.onGameUpdate(1.0)
88+
}
89+
}.toDouble(), lessThan(7.0))
90+
91+
// 3 services resolved in approximately 1/3 of what it would take if they were sequentially resolved
92+
assertThat(measureTimeMillis {
93+
services.forEach { service ->
94+
service.onGameUpdate(1.0)
95+
}
96+
}.toDouble(), `is`(both(greaterThan(80.0)).and(lessThan(120.0))))
97+
}
98+
99+
}

0 commit comments

Comments
 (0)