Jetpack Compose를 사용하여 데스크탑에 "소행성"게임을 구축하는 방법
세바스티안 에그너🏡
세비오
☄️ 나는 데스크톱을 위해 소행성을 설계했다.마우스 제어, 관성 비행, 소행성이 우주 깊은 곳에서 부서지다--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을 어떻게 사용해서 이런 체험을 재창조하는지 보고 싶습니다!
집짓기
나는 이미 대체적으로 프로젝트를 몇 개의 구성 프로젝트의 구축 블록으로 나누었는데, 우리는 이러한 구축 블록을 토론할 것이다.즉,
우리 머리를 찔러 넣자!
게임 사이클
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 invokesonFrame
with the frame time in nanoseconds in the calling context of frame dispatch, then resumes with the result fromonFrame
.
이 시리즈의 블로그 글의 두 번째 부분에서 보듯이 프레임 독립 운동에 대해 이야기할 때 프레임 시간도 도움이 될 것이다.
게임 상태 관리
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 aBox
– Compose의 가장 기본적인 레이아웃 원어 중 하나로 솔리드를 겹칠 수 있습니다(이것은 우리가 한 솔리드를 수동으로 배치하기 때문에 유용합니다).그런 다음 Jetpack Compose의 Modifier
s을 사용하여 소행성의 위치, 크기, 회전 각도, 모양(offset
)과 배경색을 CircleShape
으로 지정합니다.Compose는 이러한 기본 형태에 상당한 수준의 API를 제공하기도 합니다. 예를 들어 우리는
.rotate
을 직접 사용할 수 있으며 형상 작업을 수동으로 실행하지 않아도 실체를 정확한 방향으로 향하게 할 수 있습니다.이 코드를 최대한 간결하게 하기 위해 저는
GameObject
에 확장 함수를 도입했습니다. 게임 대상의 위치와 크기를 바탕으로 게임 대상의 편이량을 계산하는 논리를 다시 사용할 수 있습니다. 이를 xOffset
과 yOffset
이라고 합니다. 저는 몰래 앞의 코드 구간에 들어갔습니다.구현은 비교적 간단합니다.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
을 추가했다. 이 예에서는 삼각형 경로이다.전형적인 합성 방식에서 우리는 Canvas
을 Box
뒤에 있는 lambda 블록에 추가하면 Canvas
이 부모 대상의 좌표계를 삽입하고 편이와 회전을 포함한다는 것을 의미한다.그리고 이 조합체들은 하나의 게임 표면에 렌더링되었다. 단지
Box
에 불과하고, 잠금 종횡비는 1.0f
이다.물론 게임의 시각적 표현에 있어서 예술적 재능을 활용하는 것도 가능하지만 우리는 현재 그것을 최소한으로 유지하고 있다.섹션 2 계속
우리가 경기를 끝낼 때까지 해야 할 일이 조금 더 있다.본 시리즈 블로그의 두 번째 부분에서 우리는 렌더링 디테일, 게임 단순 물리 시뮬레이션 뒤의 기하학적, 선형 대수, 프레임과 무관한 운동을 더 많이 이해할 것이다.
Reference
이 문제에 관하여(Jetpack Compose를 사용하여 데스크탑에 "소행성"게임을 구축하는 방법), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/kotlin/how-i-built-an-asteroids-game-using-jetpack-compose-for-desktop-309l텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)