{"id":14969647,"url":"https://github.com/monun/heartbeat-coroutines","last_synced_at":"2025-07-23T19:04:07.069Z","repository":{"id":43713746,"uuid":"425045464","full_name":"monun/heartbeat-coroutines","owner":"monun","description":"Coroutine for Bukkit","archived":false,"fork":false,"pushed_at":"2023-08-15T12:17:25.000Z","size":149,"stargazers_count":25,"open_issues_count":4,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-09-28T12:01:39.434Z","etag":null,"topics":["asyncronous","bukkit","coroutines","java","kotlin","minecraft","papermc","spigot"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/monun.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-11-05T18:19:37.000Z","updated_at":"2024-08-05T05:18:53.000Z","dependencies_parsed_at":"2022-08-22T03:00:31.495Z","dependency_job_id":null,"html_url":"https://github.com/monun/heartbeat-coroutines","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monun%2Fheartbeat-coroutines","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monun%2Fheartbeat-coroutines/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monun%2Fheartbeat-coroutines/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monun%2Fheartbeat-coroutines/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/monun","download_url":"https://codeload.github.com/monun/heartbeat-coroutines/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":219863300,"owners_count":16555950,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncronous","bukkit","coroutines","java","kotlin","minecraft","papermc","spigot"],"created_at":"2024-09-24T13:42:10.002Z","updated_at":"2024-10-11T03:41:17.729Z","avatar_url":"https://github.com/monun.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Heartbeat Coroutines\n\n[![Kotlin](https://img.shields.io/badge/java-17-ED8B00.svg?logo=java)](https://www.azul.com/)\n[![Kotlin](https://img.shields.io/badge/kotlin-1.8.22-585DEF.svg?logo=kotlin)](http://kotlinlang.org)\n[![Gradle](https://img.shields.io/badge/kotlinx.coroutines-1.7.2-585DEF.svg)](https://github.com/Kotlin/kotlinx.coroutines)\n[![Gradle](https://img.shields.io/badge/gradle-8.2.1-02303A.svg?logo=gradle)](https://gradle.org)\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.monun/heartbeat-coroutines)](https://search.maven.org/artifact/io.github.monun/heartbeat-coroutines)\n[![GitHub](https://img.shields.io/github/license/monun/invfx)](https://www.gnu.org/licenses/gpl-3.0.html)\n[![Kotlin](https://img.shields.io/badge/youtube-각별-red.svg?logo=youtube)](https://www.youtube.com/channel/UCDrAR1OWC2MD4s0JLetN0MA)\n\n~~두근두근❤️코루틴~~\n\n### Coroutine for Paper\n\n---\n\n* #### Features\n    * Bukkit의 mainHeartBeat(GameLoop)에서 dispatch되는 Coroutine\n    * JavaPlugin 생명주기의 CoroutineScope\n    * 유연한 지연작업\n\n---\n\nGameLoop 내에서 병렬 혹은 비동기 라이브러리 없이 다음 연쇄 작업을 처리하는 코드를 작성해보겠습니다.\n\n1. 3초간 1초마다 카운트다운 메시지를 방송\n2. 5초간 1초마다 모든 개체에게 데미지\n3. `서프라이즈~` 메시지를 방송 후 종료 ~~악질 운영자~~\n\n\u003cbr\u003e\n\n#### Thread\n\n```kotlin\nRunnable {\n    repeat(3) {\n        // 비동기 문제를 해결하기 위해 GameLoop의 Thread에서 호출 \n        GameLoop.runLater {\n            broadcast(3 - it)\n            Thread.sleep(1000L)\n        }\n    }\n    repeat(5) {\n        GameLoop.runLater {\n            damageAll()\n        }\n        Thread.sleep(1000L)\n    }\n    GameLoop.runLater {\n        broadcast(\"surprise~\")\n    }\n}.let {\n    Thread(it).start()\n}\n```\n\n\u003cbr\u003e\n\n#### Callback\n\n```kotlin\n// 비동기로 어디선가 실행해주는 함수\nasync({\n    repeat(3) {\n        GameLoop.runLater {\n            broadcast(3 - it)\n            Thread.sleep(1000L)\n        }\n    }\n}) {\n    async({\n        repeat(5) {\n            GameLoop.runLater {\n                damageAll()\n            }\n            Thread.sleep(1000L)\n        }\n    }) {\n        async {\n            GameLoop.runLater {\n                broadcast(\"surprise~\")\n            }\n        }\n    }\n}\n\n```\n\n\u003cbr\u003e\n\n#### FSM\n\n```kotlin\n// 취소 가능한 태스크\nclass Surprise : GameLoopTask() {\n    private var state = 0\n    private var countdownTicks = 0\n    private var damageTicks = 0\n\n    // 1초마다 GameLoop 에서 호출\n    override fun run() {\n        when (state) {\n            0 -\u003e {\n                val message = 3 - countdownTicks++\n                broadcast(message)\n\n                if (countdownTicks \u003e= 3) state = 1\n            }\n            1 -\u003e {\n                damageAll()\n\n                if (++damageTicks \u003e= 5) state = 2\n            }\n            else -\u003e {\n                broadcast(\"surprise~\")\n                cancel()\n            }\n        }\n    }\n}\n\n```\n\n\u003cbr\u003e\n\n극단적인 예를 들었습니다만..\n\n실제로 GameLoop내에서 연쇄, 순차적인 루틴을 처리하기 위해선 대부분 위 예제들과 같은 구조의 코드를 작성하게됩니다.\n\n루틴이 복잡해질수록 비동기 문제, 복잡성으로 인해 유연성은 떨어지고 유지보수 난이도는 기하급수로 상승합니다.\n\nCoroutine을 이용하면 GameLoop 내 연쇄 작업 코드의 복잡성을 획기적으로 줄일 수 있습니다.\n\n아래 예제는 GameLoop내에서 동기적으로 실행되는 Coroutine 코드입니다\n\n\u003cbr\u003e\n\n#### Coroutine\n\n```kotlin\n// GameLoopDispatcher = GameLoop에서 Coroutine을 실행하는 CoroutineDispatcher \nCoroutineScope(GameLoopDispatcher).launch {\n    repeat(3) {\n        broadcast(3 - it)\n        delay(1000L)\n    }\n    repeat(5) {\n        damageAll()\n        delay(1000L)\n    }\n    broadcast(\"surprise~\")\n}\n```\n\nThread의 코드와 비슷하지만 GameLoop 내에서 동기적으로 실행 가능한 코드입니다!\n\nCoroutine의 동작원리는 이 [문서](https://kotlinlang.org/docs/coroutines-overview.html) 를 참고하세요\n\n---\n\n## Heartbeat coroutines 시작하기\n\n### Gradle\n\n```kotlin\nrepositories {\n    mavenCentral()\n}\n```\n\n```kotlin\ndependencies {\n    implementation(\"io.github.monun:heartbeat-coroutines:\u003cversion\u003e\")\n}\n```\n\n### Example\n\n```kotlin\n// JavaPlugin#onEnable()\nHeartbeatScope().launch {\n    val suspension = Suspension()\n    repeat(10) {\n        logger.info(server.isPrimaryThread)\n        suspension.delay(75L)\n    }\n    logger.info(\"BOOM\")\n}\n```\n\n---\n\n### Dispatchers.Heartbeat\n\n`JavaPlugin` 과 같은 생명주기를 가진 `CoroutineDispatcher` 입니다.\n\nCoroutine을 Bukkit의 PrimaryThread에서만 실행합니다.\n\n---\n\n### HeartbeatScope()\n\n`JavaPlugin` 과 같은 생명주기를 가진 `CoroutineScope` 입니다.\n\n`Dispatchers.Heartbeat`를 `CoroutineDispatcher`로 가지며 `JavaPlugin` 생명주기를 따라가는 `SupervisorJob`을 부모로 가집니다.\n\n---\n\n### Suspension\n\n누적 지연 기능을 가진 클래스입니다.\n\n`Dispatchers.Heartbeat`는 Coroutine을 Bukkit의 PrimaryThread에서 실행하기 위해서 `BukkitScheduler#runTask`를 사용합니다.\n\n`BukkitScheduler`는 1tick(50ms)마다 등록된 태스크들을 실행하며 서버 상태에 따라 지연될 수 있습니다.\n\nCoroutine은 지연을 millisecond 단위로 제어 할 수 있으며 이는 `Dispatchers.Heartbeat`에서 실행될 때 결과가 기대와 다를 수 있습니다.\n\n`delay(1)` 함수가 호출 될 때 `Dispatchers.Heartbeat`에서는 50ms 이상 늘어날 수 있습니다.\n\n`Suspension`은 내부적으로 누적되는 지연 시간을 가지며 누적된 시간이 과거일 경우 yield()를 호출하고 미래일 경우 남은 시간만큼 `delay`를 호출합니다. \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmonun%2Fheartbeat-coroutines","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmonun%2Fheartbeat-coroutines","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmonun%2Fheartbeat-coroutines/lists"}