저번에 웹에 만들었던 지도와 비슷한 것을 Compose Multiplatform 을 사용해 데스크탑 환경에서 사용할 수 있도록 한 후기.
아무리 생각해도 저번에 만들었던 것은 속도가 너무 느리고 아무래도 해보려던 것을 많이 못해본 느낌이 강했다.
그러나 그걸 제대로 해보려면 웹이라는 플랫폼은 적절하지 않다고 판단한 바, 데스크탑 환경에서 진행해보기로 했다.
사실 이 글 쓰는 시점이 프로젝트 샷다 내리고 꽤 시간이 지난 이후이기도 하고, 정신이 맑은 상태가 아니어서 다소 두서가 없다.
솔직히 샷다 내린 지 두 달이나 지나서... 분명 많은 걸 했는데 하나도 생각이 안난다...
이번 개발은 크게 몇 가지 목표를 세우고 진행했다. 특히 지도 관련 목표는 아래와 같았다:
기존에 했던 구현에서는, ChunkSnapshot 을 쓴 것 까지는 좋았으나 마인크래프트의 개쩌는 압축 방식의 뽕을 지나치게 맞은게 문제였다.
굳이 이미 압축되어있지 않은 데이터를 다시 압축하고, 압축했어도 충분히 큰 데이터를 프론트에 네트워크를 통해 전달하고, 그걸 프론트에서 다시 압축을 풀고, 그 다음에서야 canvas 엘리먼트를 통해 한픽셀 한픽셀을 그렸다.
즉, 그냥 압축되어있지 않은 데이터로 바로 서버에서 이미지를 렌더링한 후에 렌더링된 이미지 자체를 프론트로 보내는게 훨씬 빠르다.
게다가 서버에서 렌더링을 하면 Coroutine 등을 통해 더 속도를 향상시킬 수 있다는 장점이 있었다.
실질 기존 구현에서 압축한 데이터가 거의 서버에서 렌더링한 Jpg 이미지 파일의 크기에 맞먹었으니 말 다 한 것이나 다름없다...
우선 Java 의 BufferedImage 와 ImageIO 를 사용했다.
최적화부터 하고 기능을 추가하는건 바람직하지 않다고 판단해서 일단 넣고싶었던 렌더링 관련 기능을 전부 먼저 때려넣었다.
BlockLight 및 SkyLight 관련 사항, 석양 관련 사항을 모두 추가했다.
빛 관련 사항은 다행히 이 값들이 실시간 계산이 아닌 ChunkSnapshot 을 통해 바로 가져올 수 있는 값이어서, 반영에는 크게 문제가 있지 않았다.
방위 계산도 사실 좌표 변환 관련해서 너무 많은 삽질을 했지만 이건 단순히 내 머리가 나빠서이기 때문에 넘어가려고 한다 여기에 -를 붙혀볼까? 이걸 +로 바꾸면 되나?
바이옴은... 마인크래프트 위키에 기술된 내용을 바탕으로 바이옴 별 나뭇잎/잔디 색을 상수화 해서 해결했다.
문제는 석양이었다.
석양이 질 때, 단순히 주황색 그레디언트를 바로 덧대면 너무 없어보여서, BlendMode 를 사용하고자 했다.
원래 이 기능은 실제로 색상 간의 계산을 하려면 복잡하고 프론트에서는 알아서 잘 해주니 프론트에서 시간 값을 받아 표시하려고 했는데, MacOS 에서 JetpackCompose 의 BlendMode 가 이상동작을 해서... 결국 직접 렌더링에 포함해야했다.
사용하려고 한 건 Overlay 모드였는데, 뭔가의 공식이 있긴 했는데 src 와 dst 중 하나에 alpha 가 포함되면 꼬이기 시작했다.
사실 이 삽질의 과정은 명확히 남겨두지 않았기도 하고, 공식을 헷갈려서 코드로 잘못 옮겼거나 자료형 변환 중에 뭔가 잘렸(truncated)거나 이런 이슈여서 스킵하도록 하고... 참고한 문서의 링크와 최종적으로 옮긴 코드만 남기려고 한다:
W3C 문서 만세!
val blend: (Float, Float) -> Float = { src, dst -> if (src <= 0.5) dst * 2 * src else 1 - ((1 - dst) * (1 - (2 * src - 1))) } val f1: (Float, Float) -> Float = { src, dst -> (1 - srcA) * dst + srcA * blend(src, dst) } val f2: (Float, Float) -> Float = { src, dst -> dstA * 1 * dst + srcA * (1 - dstA) * src }
blend 람다는 문서 상에서 overlay 섹션의 B(Cb, Cs) 에 해당하고, f1, f2 는 각각 6번 섹션에 기술된 Cs, Co 에 해당한다.
사실 문서를 읽는게 너무 어려웠다. 문서에 보이는 단어들도 굉장히 생소하고, 어느게 어느걸 의미하는지도 모호했다.
as, Fa, Cs, ab, Fb, Cb 이런게 다 각각 뭔지 어케 아냐 이말이다...
아무튼 삽질이 끝나고 석양 기능까지 반영했다.
이후 가장 단순하다고 생각한 형태로 렌더링을 진행한 바, 놀랍게도 이 많은 기능을 넣었는데도 기존에 했던 구현보다 빨랐다.
그러나 물론 실사용으로 쓰기엔 아직 빠르지 않았다. 그래서 여러 가지 최적화 옵션들을 더했다.
ChunkSnapshot.getHighestBlockYAt(int, int)
로부터 시작하도록 했다.그러나 둘 모두 항상 적용되는 사항이 아니기 때문에, 실질 그렇게 큰 시간이 단축되지는 않았다.
그리하여 이번에는 극단의 조치로 ThreadPool
을 사용하기로 한다.
기존 구현을 그대로 사용했다면 좀 더 단순했을 것이다. 왜냐하면 기존 구현은 특정 픽셀을 그릴 때 바로 위의 픽셀에 대한 정보에만 의존했기 때문에, 위에서 아래로 그리는 세로로 긴 한 줄을 하나의 스레드에 맡기면 이론상 1 / 이미지 가로 너비 만큼 속도를 개선시키는 것이 가능했다.
그러나 이번 구현은 바로 위쪽 픽셀 뿐만이 아니라, 바이옴에 따라 왼쪽 픽셀에도 영향을 받을 수 있게 되었기 때문에, 위처럼 하면 Race-Condition이 발생하여 아직 그려지지 않은 부분을 참조할 가능성도 있었다.
그러나 이거는 로직의 흐름을 수정하여 막기에는 무리가 있다고 판단, 참조하려는 픽셀이 아직 그려지지 않은 상태라면 해당 스레드는 그 픽셀이 그려질 때까지 대기하도록 했다.
스레드 풀의 수 16개 기준, 이렇게 했을 때 기존에 걸리던 시간보다 1/10 정도로 줄었다.
그리고, 시간 측정을 항상 Postman 으로 요청을 보낸 이후 완전히 응답을 받기까지의 시간으로 했었는데, 그게 의도한 값과는 달랐다.
사실 내가 측정하려는 것은 '순수 렌더링에 걸린 시간' 이기 때문에 '렌더링 결과를 네트워크를 통해 프론트에 전달하는 시간'은 포함하면 안됐는데, Postman 이 알려주는 시간은 내 요청이 출발한 시간으로부터 모든 응답이 완전히 내게로 돌아올 때까지 걸린 시간이었기 때문에.
그래서 서버 코드에서 measureTimeMillis { }
를 사용해 측정한 결과 진짜 걸린 시간을 알 수 있었다.
그 값은...
27ms
물론 네트워크 통신 시간이 100ms 갸랑 추가되기는 하지만, 그래도 꽤 만족스러운 성과라고 생각했다.
기존 구현에서는... 서버도 대략 3-5초동안 일하고... 주고받는 데이터 크기도 JSON인데도 이미지보다 크고... 그걸 또 복잡하게 풀어서 그리고 그랬는데.
이것 저것 많은 기능을 추가하고도 이렇게밖에 안걸린다니. 와!
이번에도 Jetpack Compose 를 사용했다.
마인크래프트 서버 모니터라는 컨셉으로, 여러 위치의 지도나 접속 플레이어 목록, 서버의 CPU 및 메모리 사용량 등을 표시하고자 했다.
그리고, 모니터에 표시할 요소들을 커스터마이징 할 수 있게 했다. 약간 대시보드처럼 UI를 구성했는데, 그 구성 요소를 추가하거나 제거하거나 순서를 변경할 수 있게끔!
그리고 최종적으로 여러 서버를 프로그램 종료 없이 재핑해가면서 확인할 수 있게까지 했다.
사실 실시간 업데이트가 상당히 서버에 부하를 주는 작업이라 크게 의미는 없지만, 그래도 나름 쓸만한 무언가가 나왔다.
이번에는 MacOS 도 지원하기 위해 dmg 도 만들고 애플 개발자 플랜도 가입하고 개비싸 코드 검증(notarize)도 받고 이것저것 열심히 했다.
UI 자체도 나름 신경써서 만들었고 그런 것 치고는 너무 실사용이 어렵게 나왔음... 서버도 여러가지로 깔끔하게 짜려고 했다.
사실 프론트에서 기획이 자주 틀어졌는데, 코딩이야 그렇다 치더라도 기획은 처음에 잘 잡고 가는게 나은 것 같다.
사실 기록을 좀 더 꼼꼼히 남기고 이 글을 프로젝트 샷다 내린 시점에 썼다면 참 좋았을텐데.
분명 많은 삽질과 많은 무언가를 했는데 기억나는게 없다...