Jetpack Compose를 사용하여 데스크탑에 "소행성"게임을 구축하는 방법

얼마 전 트위터에 제가 Jetpack Compose for Desktop에 만든 작은 게임: 클래식 버스킹 게임 Asteroids의 작은 복제본을 발표했습니다. 이 게임에서 당신은 마우스로 우주선 한 척을 제어하여 드넓은 우주에서 내비게이션을 할 수 있습니다. 이 과정에서 소행성을 피하고 파괴할 수 있습니다.



세바스티안 에그너🏡
세비오

☄️ 나는 데스크톱을 위해 소행성을 설계했다.마우스 제어, 관성 비행, 소행성이 우주 깊은 곳에서 부서지다--CFD 위에서!GitHub에서 게임 소스 코드를 찾으십시오: github.com/SebastianAigne… 많은 즐거움과 학습.위에 블로그를 써야 돼!😄
오후 12:15~2021년 4월 15일
오늘, 내가 어떻게 이 게임의 기본 버전을 구축했는지, 그리고Compose for Desktop이 어떻게 내가 하룻밤에 그것을 실현하도록 도와주었는지 이해할 때가 되었다!
우리는 코드에서 내가 가장 재미있게 생각하는 부분과 구조를 볼 것이다.이 모든 것이 어떻게 결합되었는지 알아보기 위해서, 나는 whole code on GitHub을 탐색할 것을 건의합니다.전체 실현은 300줄 코드밖에 없는데, 나는 이것이 학습과 이해를 쉽게 할 수 있기를 바란다.

플레이


만약 당신이 80년대의 비행기 게임에 아직 빠져 있지 않다면, Asteroids은 유행하는 비행기 게임이다. 당신은 우주선을 조종하여 우주를 통과하고, 우주선으로 소행성을 피하고 파괴해 볼 수 있다.
당시 하드웨어의 제한으로 인해 게임의 외관은 매우 간단했다. 삼각형의 우주선이 일반적인 배경에서 이동하여 2D 표면에 소행성을 간단하게 표시하는 것을 피했다.
이것이 도전이 되는 것은 인터내셔널이다. 진정한 우주선처럼 당신의 우주선이 일정한 속도로 노선을 따라 직선으로 이동하려면 당신의 우주선을 돌리고 당신의 추력을 인도함으로써 교정 동작을 해야 한다.
소행성은 아케이드 게임에서 숭배의 지위를 얻었다.그래서 저는 Jetpack Compose for Desktop을 어떻게 사용해서 이런 체험을 재창조하는지 보고 싶습니다!

집짓기


나는 이미 대체적으로 프로젝트를 몇 개의 구성 프로젝트의 구축 블록으로 나누었는데, 우리는 이러한 구축 블록을 토론할 것이다.즉,
  • The Game Loop
  • Game State Management
  • Rendering to the Screen
  • 이 시리즈는 Compose for Desktop을 사용하여 게임을 구축하는 두 번째 부분에서 다른 렌더링 디테일, 게임 뒤의 기하학적 대수와 선형 대수, 프레임과 무관한 운동도 이해할 것이다.
    우리 머리를 찔러 넣자!

    게임 사이클

    At the center of most games stands the game loop. It acts as the entry point that calls the game logic code. This is a fundamental difference between implementing typical declarative user interfaces and building games:

    • Declarative UI is usually mostly static, and reacts to user actions (clicking, dragging) or other events (new data, computation progress...)
    • Games run their logic many times per second, simulating the game world and its entities one frame at a time.

    That is not to say that these two approaches are incompatible! All we need to run a main "game loop" is to get our function to execute once per frame. In Jetpack Compose, we have the withFrame family of functions ( withFrameMillis , withFrameNanos ), which can help us achieve exactly that.

    Let's assume we already have a game object – we will talk about state management shortly. We can then create a LaunchedEffect which asks Jetpack Compose for Desktop to call our update function whenever a new frame is rendered:

    LaunchedEffect(Unit) {
        while (true) {
            withFrameNanos {
                game.update(it)
            }
        }
    }
    
    withFrameNanos is a suspending method. Its exact implementation is described in the documentation :

    withFrameNanos suspends until a new frame is requested, immediately invokes onFrame with the frame time in nanoseconds in the calling context of frame dispatch, then resumes with the result from onFrame.


    이 시리즈의 블로그 글의 두 번째 부분에서 보듯이 프레임 독립 운동에 대해 이야기할 때 프레임 시간도 도움이 될 것이다.

    게임 상태 관리

    Jetpack Compose is excellent at managing state, and when building a game like Asteroids, we can use the same mechanisms to keep track of the data attached to game objects or the current play session, to name just two examples.

    As suggested in the previous section, my Asteroids game has a Game class, an instance of which is wrapped in a remember call in the main composition.

    val game = remember { Game() }
    

    It acts as a container for all game-related data. For example:

    • Game object information (in a mutableStateListOf )
    • The current game phase ( RUNNING / STOPPED )
    • The size of the playing field (based on window dimensions)

    Inside the Game object, we treat the state data as mutable, and make any state changes as we see fit.

    게임 아이템


    단일 게임 대상은 다시 한 번 단일 게임 실체에 속하는 상태를 그룹으로 나눈다. 우주선, 소행성 또는 총알이다. 그리고 그 상태를 수정하고 새로운 게임 대상을 생성하거나 다른 게임 대상과의 관계를 검사하는 방법을 제공한다.
    내가 소행성에 대한 실현에서 모든 게임 대상은 공통된 행동을 많이 한다. 소행성들이 환경에서 이동하는 방식부터 충돌을 어떻게 검사하는지까지. 우리는 잠시 후에 기하학적, 선형적 대수를 토론할 것이다.GameObject 클래스는 다음과 같은 공유 동작을 제공합니다.
    sealed class GameObject(speed: Double = 0.0, angle: Double = 0.0, position: Vector2 = Vector2.ZERO) {
        var speed by mutableStateOf(speed)
        var angle by mutableStateOf(angle)
        var position by mutableStateOf(position)
        var movementVector /* ... */
        abstract val size: Double
    
        fun update(realDelta: Float, game: Game) {
            val velocity = movementVector * realDelta.toDouble()
            position += velocity
            position = position.mod(Vector2(game.width.value.toDouble(), game.height.value.toDouble()))
        }
    
        fun overlapsWith(other: GameObject): Boolean {
            return this.position.distanceTo(other.position) < (this.size / 2 + other.size / 2)
        }
    }
    
    예를 들어 ShipData류는 speed류에서 angle, position, update과 그의 GameObject 방법을 계승하였으나 그 자체의 크기, 각도와 총알을 발사하는 함수를 정의하였다.
    class ShipData : GameObject() {
        override var size: Double = 40.0
        var visualAngle: Double = 0.0
    
        fun fire(game: Game) {
            val ship = this
            game.gameObjects.add(BulletData(ship.speed * 4.0, ship.visualAngle, ship.position))
        }
    }
    
    ShipData(일반적으로 GameObject)은 이 프로젝트를 디스플레이에 어떻게 표시하는지에 대한 논리를 포함하지 않습니다. Jetpack Compose를 사용하면 상태와 디스플레이를 분리하는 것이 매우 쉽습니다.
    게임에서 모든 유형의 실체는 많은 행위를 하기 때문에 우리의 메인 게임 순환은 그것들을 슈퍼 유형 GameObject으로 볼 수 있고 특정한 유형의 대상 간의 특정한 상호작용, 예를 들어 총알 소행성이나 소행성 유저 충돌을 전문적으로 처리할 수 있다.

    화면으로 렌더링

    I found that in Jetpack Compose, separating game data from the visual representation comes quite naturally. Game objects like a ship, an asteroid, or a bullet are all represented in two parts:

    • A class holding the state associated with the game object (in terms of "Compose state" – via mutableStateOf and friends) – We briefly talked about this in the previous section.
    • A @Composable , defining the rendering based on the game object's data.

    To illustrate the latter, here's the minimal visual representation of the Asteroid composable. It receives asteroidData , which is the container for all information regarding the state of this particular game object:

    @Composable
    fun Asteroid(asteroidData: AsteroidData) {
        val asteroidSize = asteroidData.size.dp
        Box(
            Modifier
                .offset(asteroidData.xOffset, asteroidData.yOffset)
                .size(asteroidSize)
                .rotate(asteroidData.angle.toFloat())
                .clip(CircleShape)
                .background(Color(102, 102, 153))
        )
    }
    

    This code snippet is enough to describe the whole visual representation of an asteroid.

    We start with a Box – Compose의 가장 기본적인 레이아웃 원어 중 하나로 솔리드를 겹칠 수 있습니다(이것은 우리가 한 솔리드를 수동으로 배치하기 때문에 유용합니다).그런 다음 Jetpack Compose의 Modifier s을 사용하여 소행성의 위치, 크기, 회전 각도, 모양(offset)과 배경색을 CircleShape으로 지정합니다.
    Compose는 이러한 기본 형태에 상당한 수준의 API를 제공하기도 합니다. 예를 들어 우리는 .rotate을 직접 사용할 수 있으며 형상 작업을 수동으로 실행하지 않아도 실체를 정확한 방향으로 향하게 할 수 있습니다.
    이 코드를 최대한 간결하게 하기 위해 저는 GameObject에 확장 함수를 도입했습니다. 게임 대상의 위치와 크기를 바탕으로 게임 대상의 편이량을 계산하는 논리를 다시 사용할 수 있습니다. 이를 xOffsetyOffset이라고 합니다. 저는 몰래 앞의 코드 구간에 들어갔습니다.구현은 비교적 간단합니다.
    val GameObject.xOffset: Dp get() = position.x.dp - (size.dp / 2)
    val GameObject.yOffset: Dp get() = position.y.dp - (size.dp / 2)
    
    더 복잡한 조합은 Ship부품으로 삼각형과 원형의 형상을 결합시켜 매우 간단한 우주선을 만들었다.
    @Composable
    fun Ship(shipData: ShipData) {
        val shipSize = shipData.size.dp
        Box(
            Modifier
                .offset(shipData.xOffset, shipData.yOffset)
                .size(shipSize)
                .rotate(shipData.visualAngle.toFloat())
                .clip(CircleShape)
                .background(Color.Black)
        ) {
            Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
                drawPath(
                    color = Color.White,
                    path = Path().apply {
                        val size = shipSize.toPx()
                        moveTo(0f, 0f) // Top-left corner...
                        lineTo(size, size / 2f) // ...to right-center...
                        lineTo(0f, size) // ... to bottom-left corner.
                    }
                )
            })
        }
    }
    
    비행선을 정의하는 Box은 우리가 본 Asteroid과 매우 비슷하지만, 우리는 비행선의 꼭대기에 추가적인 모양을 그릴 수 있도록 Canvas을 추가했다. 이 예에서는 삼각형 경로이다.전형적인 합성 방식에서 우리는 CanvasBox 뒤에 있는 lambda 블록에 추가하면 Canvas이 부모 대상의 좌표계를 삽입하고 편이와 회전을 포함한다는 것을 의미한다.
    그리고 이 조합체들은 하나의 게임 표면에 렌더링되었다. 단지 Box에 불과하고, 잠금 종횡비는 1.0f이다.물론 게임의 시각적 표현에 있어서 예술적 재능을 활용하는 것도 가능하지만 우리는 현재 그것을 최소한으로 유지하고 있다.

    섹션 2 계속


    우리가 경기를 끝낼 때까지 해야 할 일이 조금 더 있다.본 시리즈 블로그의 두 번째 부분에서 우리는 렌더링 디테일, 게임 단순 물리 시뮬레이션 뒤의 기하학적, 선형 대수, 프레임과 무관한 운동을 더 많이 이해할 것이다.

    좋은 웹페이지 즐겨찾기