Heartbeat Coroutines
두근두근
Coroutine for Paper
-
Features
- Bukkit의 mainHeartBeat(GameLoop)에서 dispatch되는 Coroutine
- JavaPlugin 생명주기의 CoroutineScope
- 유연한 지연작업
GameLoop 내에서 병렬 혹은 비동기 라이브러리 없이 다음 연쇄 작업을 처리하는 코드를 작성해보겠습니다.
- 3초간 1초마다 카운트다운 메시지를 방송
- 5초간 1초마다 모든 개체에게 데미지
서프라이즈~
메시지를 방송 후 종료악질 운영자
Thread
Runnable {
repeat(3) {
// 비동기 문제를 해결하기 위해 GameLoop의 Thread에서 호출
GameLoop.runLater {
broadcast(3 - it)
Thread.sleep(1000L)
}
}
repeat(5) {
GameLoop.runLater {
damageAll()
}
Thread.sleep(1000L)
}
GameLoop.runLater {
broadcast("surprise~")
}
}.let {
Thread(it).start()
}
Callback
// 비동기로 어디선가 실행해주는 함수
async({
repeat(3) {
GameLoop.runLater {
broadcast(3 - it)
Thread.sleep(1000L)
}
}
}) {
async({
repeat(5) {
GameLoop.runLater {
damageAll()
}
Thread.sleep(1000L)
}
}) {
async {
GameLoop.runLater {
broadcast("surprise~")
}
}
}
}
FSM
// 취소 가능한 태스크
class Surprise : GameLoopTask() {
private var state = 0
private var countdownTicks = 0
private var damageTicks = 0
// 1초마다 GameLoop 에서 호출
override fun run() {
when (state) {
0 -> {
val message = 3 - countdownTicks++
broadcast(message)
if (countdownTicks >= 3) state = 1
}
1 -> {
damageAll()
if (++damageTicks >= 5) state = 2
}
else -> {
broadcast("surprise~")
cancel()
}
}
}
}
극단적인 예를 들었습니다만..
실제로 GameLoop내에서 연쇄, 순차적인 루틴을 처리하기 위해선 대부분 위 예제들과 같은 구조의 코드를 작성하게됩니다.
루틴이 복잡해질수록 비동기 문제, 복잡성으로 인해 유연성은 떨어지고 유지보수 난이도는 기하급수로 상승합니다.
Coroutine을 이용하면 GameLoop 내 연쇄 작업 코드의 복잡성을 획기적으로 줄일 수 있습니다.
아래 예제는 GameLoop내에서 동기적으로 실행되는 Coroutine 코드입니다
Coroutine
// GameLoopDispatcher = GameLoop에서 Coroutine을 실행하는 CoroutineDispatcher
CoroutineScope(GameLoopDispatcher).launch {
repeat(3) {
broadcast(3 - it)
delay(1000L)
}
repeat(5) {
damaegAll()
delay(1000L)
}
brocast("surprise~")
}
Thread의 코드와 비슷하지만 GameLoop 내에서 동기적으로 실행 가능한 코드입니다!
Coroutine의 동작원리는 이 문서 를 참고하세요
Heartbeat coroutines 시작하기
Gradle
repositories {
mavenCentral()
}
dependencies {
implementation("io.github.monun:heartbeat-coroutines:<version>")
}
Example
// JavaPlugin#onEnable()
HeartbeatScope().launch {
val suspension = Suspension()
repeat(10) {
logger.info(server.isPrimaryThread)
suspension.delay(75L)
}
logger.info("BOOM")
}
Dispatchers.Heartbeat
JavaPlugin
과 같은 생명주기를 가진 CoroutineDispatcher
입니다.
Coroutine을 Bukkit의 PrimaryThread에서만 실행합니다.
HeartbeatScope()
JavaPlugin
과 같은 생명주기를 가진 CoroutineScope
입니다.
Dispatchers.Heartbeat
를 CoroutineDispatcher
로 가지며 JavaPlugin
생명주기를 따라가는 SupervisorJob
을 부모로 가집니다.
Suspension
누적 지연 기능을 가진 클래스입니다.
Dispatchers.Hearbeat
는 Coroutine을 Bukkit의 PrimaryThread에서 실행하기 위해서 BukkitScheduler#runTask
를 사용합니다.
BukkitScheduler
는 1tick(50ms)마다 등록된 태스크들을 실행하며 서버 상태에 따라 지연될 수 있습니다.
Coroutine은 지연을 millisecond 단위로 제어 할 수 있으며 이는 Dispatchers.Heartbeat
에서 실행될 때 결과가 기대와 다를 수 있습니다.
delay(1)
함수가 호출 될 때 Dispatchers.Heatbeat
에서는 50ms 이상 늘어날 수 있습니다.
Suspension
은 내부적으로 누적되는 지연 시간을 가지며 누적된 시간이 과거일 경우 yield()를 호출하고 미래일 경우 남은 시간만큼 delay
를 호출합니다.