Skip to content

Commit

Permalink
feat(coroutines): Implements method to await physics and process frames
Browse files Browse the repository at this point in the history
  • Loading branch information
piiertho committed Oct 13, 2024
1 parent 8351024 commit 3b22b75
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 45 deletions.
2 changes: 2 additions & 0 deletions harness/tests/scripts/godot/tests/coroutine/CoroutineTest.gdj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ functions = [
start_coroutine_with_one_parameter,
start_coroutine_with_many_parameters,
start_coroutine_undispatched,
start_coroutine_with_physics_frame,
start_coroutine_with_process_frame,
run_on_main_thread_from_background_thread,
async_load_resource
]
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import godot.core.Vector2
import godot.core.signal0
import godot.core.signal1
import godot.core.signal4
import godot.coroutines.godotCoroutine
import godot.coroutines.await
import godot.coroutines.awaitLoadAs
import godot.coroutines.awaitMainThread
import godot.coroutines.awaitPhysicsFrame
import godot.coroutines.awaitProcessFrame
import godot.coroutines.godotCoroutine
import godot.global.GD
import kotlinx.coroutines.CoroutineStart

Expand Down Expand Up @@ -61,6 +63,22 @@ class CoroutineTest : Object() {
step = 8
}

@RegisterFunction
fun startCoroutineWithPhysicsFrame() = godotCoroutine(start = CoroutineStart.UNDISPATCHED) {
step = 9
awaitPhysicsFrame {
step = 10
}
}

@RegisterFunction
fun startCoroutineWithProcessFrame() = godotCoroutine(start = CoroutineStart.UNDISPATCHED) {
step = 11
awaitProcessFrame {
step = 12
}
}

@RegisterSignal
val runOnMainThreadFromBackgroundThreadFinished by signal1<Boolean>("is_test_successful")

Expand Down
97 changes: 53 additions & 44 deletions harness/tests/test/unit/test_coroutines.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,56 @@ extends "res://addons/gut/test.gd"


func test_coroutine_await():
var test_script: Object = CoroutineTest.new()

assert_eq(test_script.step, 0, "Property should be 0 at first.")

test_script.start_coroutine_without_parameter()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 1, "Property should be 1 after coroutine started but waiting.")

test_script.signal_without_parameter.emit()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 2, "Property should be 2 after coroutine ran and signal triggered.")

test_script.start_coroutine_with_one_parameter()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 3, "Property should be 3 after coroutine started but waiting.")

test_script.signal_with_one_parameter.emit(4)
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 4, "Property should be 4 after coroutine ran.")

test_script.start_coroutine_with_many_parameters()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 5, "Property should be 5 after coroutine started but waiting.")

test_script.signal_with_many_parameters.emit(6, 0.1, Vector2(0,0), "test")
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 6, "Property should be 6 after coroutine ran.")

test_script.start_coroutine_undispatched()
assert_eq(test_script.step, 7, "Property should be immediately 7 when coroutine is undispatched.")

test_script.signal_without_parameter.emit()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 8, "Property should be 8 when coroutine is resumed.")

test_script.run_on_main_thread_from_background_thread()
var run_on_main_thread_from_background_thread_success = await test_script.run_on_main_thread_from_background_thread_finished
assert_true(run_on_main_thread_from_background_thread_success, "Code should be executed on the correct threads")

test_script.async_load_resource()
var async_load_resource_success = await test_script.async_load_resource_finished
assert_true(async_load_resource_success, "Resource should be loaded")

test_script.free()
var test_script: Object = CoroutineTest.new()

assert_eq(test_script.step, 0, "Property should be 0 at first.")

test_script.start_coroutine_without_parameter()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 1, "Property should be 1 after coroutine started but waiting.")

test_script.signal_without_parameter.emit()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 2, "Property should be 2 after coroutine ran and signal triggered.")

test_script.start_coroutine_with_one_parameter()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 3, "Property should be 3 after coroutine started but waiting.")

test_script.signal_with_one_parameter.emit(4)
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 4, "Property should be 4 after coroutine ran.")

test_script.start_coroutine_with_many_parameters()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 5, "Property should be 5 after coroutine started but waiting.")

test_script.signal_with_many_parameters.emit(6, 0.1, Vector2(0,0), "test")
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 6, "Property should be 6 after coroutine ran.")

test_script.start_coroutine_undispatched()
assert_eq(test_script.step, 7, "Property should be immediately 7 when coroutine is undispatched.")

test_script.signal_without_parameter.emit()
await get_tree().create_timer(1).timeout
assert_eq(test_script.step, 8, "Property should be 8 when coroutine is resumed.")

test_script.start_coroutine_with_physics_frame()
await get_tree().physics_frame
assert_eq(test_script.step, 10, "Property should be 10 when coroutine is resumed.")

test_script.start_coroutine_with_process_frame()
await get_tree().process_frame
assert_eq(test_script.step, 12, "Property should be 12 when coroutine is resumed.")

test_script.run_on_main_thread_from_background_thread()
var run_on_main_thread_from_background_thread_success = await test_script.run_on_main_thread_from_background_thread_finished
assert_true(run_on_main_thread_from_background_thread_success, "Code should be executed on the correct threads")

test_script.async_load_resource()
var async_load_resource_success = await test_script.async_load_resource_finished
assert_true(async_load_resource_success, "Resource should be loaded")

await get_tree().create_timer(1).timeout
test_script.free()
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package godot.coroutines

import kotlinx.coroutines.async

suspend inline fun <R> awaitProcessFrame(
crossinline block: () -> R
): R {
val job = GodotCoroutine.async(GodotDispatchers.ProcessFrame) {
block()
}
return job.await()
}

suspend inline fun <R> awaitPhysicsFrame(
crossinline block: () -> R
): R {
val job = GodotCoroutine.async(GodotDispatchers.PhysicsFrame) {
block()
}

return job.await()
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package godot.coroutines

import godot.Engine
import godot.Object
import godot.SceneTree
import godot.WorkerThreadPool
import godot.core.Callable
import godot.core.asCallable
import godot.core.connect
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Runnable
import kotlin.coroutines.CoroutineContext

object GodotDispatchers {

val MainThread: CoroutineDispatcher = GodotMainThreadCoroutineDispatcher
val ThreadPool: CoroutineDispatcher = GodotThreadPoolCoroutineDispatcher
val ProcessFrame: CoroutineDispatcher = GodotProcessFrameCoroutineDispatcher
val PhysicsFrame: CoroutineDispatcher = GodotPhysicsFrameCoroutineDispatcher

private object GodotMainThreadCoroutineDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
Expand All @@ -22,4 +29,30 @@ object GodotDispatchers {
WorkerThreadPool.addTask({ block.run() }.asCallable())
}
}

private object GodotProcessFrameCoroutineDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
sceneTree.processFrame.connect(Object.ConnectFlags.CONNECT_ONE_SHOT.id.toInt()) {
block.run()
}
}
}

private object GodotPhysicsFrameCoroutineDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
sceneTree.physicsFrame.connect(Object.ConnectFlags.CONNECT_ONE_SHOT.id.toInt()) {
block.run()
}
}
}

private val sceneTree by lazy {
val tree = Engine.getMainLoop()

require(tree is SceneTree) {
"Your main loop should be a scene tree to use ${GodotProcessFrameCoroutineDispatcher::class}."
}

return@lazy tree
}
}

0 comments on commit 3b22b75

Please sign in to comment.