当前位置:网站首页>Remember a compose version of Huarong Road, you deserve it!

Remember a compose version of Huarong Road, you deserve it!

2022-06-23 22:31:00 User 9239674

The basic idea

The logic of the game is relatively simple , So I didn't use MVI A framework like that , But the whole is still data-driven UI Design idea :

  1. Define the state of the game
  2. State based UI draw
  3. User input triggers a state change

1. Define the game state

The state of the game is very simple , That is, the current pieces (Chees) Where are you going , So you can put the of a chess piece List As a carrier State Data structure of

1.1 Chess piece definition

Let's first look at the definition of a single piece

data class Chess(
    val name: String, // Character name 
    val drawable: Int // Character picture 
    val w: Int, // The width of the piece 
    val h: Int, // The length of the pieces 
    val offset: IntOffset = IntOffset(0, 0) // Offset 
)

adopt w,h You can determine the shape of the chess piece ,offset Determine the current position in chess and cards

1.2 The chess pieces are placed at the beginning of the game

Next, we define the pieces of each character , And put these pieces according to the opening state

val zhang = Chess(" Zhang Fei ", R.drawable.zhangfei, 1, 2)
val cao = Chess(" Cao Cao ", R.drawable.caocao, 2, 2)
val huang = Chess(" Huang Zhong ", R.drawable.huangzhong, 1, 2)
val zhao = Chess(" zhaoyun ", R.drawable.zhaoyun, 1, 2)
val ma = Chess(" d ", R.drawable.machao, 1, 2)
val guan = Chess(" Guan yu ", R.drawable.guanyu, 2, 1)
val zu = buildList {  repeat(4) { add(Chess(" pawn $it", R.drawable.zu, 1, 1)) } }

Define the shape of the chess pieces in the definition of each role , such as “ Zhang Fei ” The aspect ratio is 2:1,“ Cao Cao ” The aspect ratio is 2:2.

Next, define a game start :

val gameOpening: List<Triple<Chess, Int, Int>> = buildList {
    add(Triple(zhang, 0, 0)); add(Triple(cao,   1, 0))
    add(Triple(zhao,  3, 0)); add(Triple(huang, 0, 2))
    add(Triple(ma,    3, 2)); add(Triple(guan,  1, 2))
    add(Triple(zu[0], 0, 4)); add(Triple(zu[1], 1, 3))
    add(Triple(zu[2], 2, 3)); add(Triple(zu[3], 3, 4))}

Triple The three members of represent the piece and its offset in the chessboard , for example Triple(cao, 1, 0) It means that Cao Cao started at (1,0) coordinate .

Finally, through the following code , take gameOpening Into what we need State, That is, a List<Chess>:

const val boardGridPx = 200 // Chess piece unit size 
fun ChessOpening.toList() =
    map { (chess, x, y) ->
        chess.moveBy(IntOffset(x * boardGridPx, y * boardGridPx))
    }

2. UI Rendering , Draw a chess game

With List<Chess> after , Draw the pieces in turn , So as to complete the drawing of the whole chess game .

@Composable
fun ChessBoard (chessList: List<Chess>) {
    Box(
        Modifier
            .width(boardWidth.toDp())
            .height(boardHeight.toDp())
    ) {
        chessList.forEach { chess ->
             Image( // Picture of chess pieces 
                    Modifier
                        .offset { chess.offset } // Offset position 
                        .width(chess.width.toDp()) // The width of the piece 
                        .height(chess.height.toDp())) // Chess piece height 
                    painter = painterResource(id = chess.drawable),
                    contentDescription = chess.name
             )
        }
    }
}

Box Determine the range of the chessboard ,Image Draw chess pieces , And pass Modifier.offset{ } Put it in the right place .

Only this and nothing more , We use Compose Draw a static start , The next step is to move the chess pieces with your fingers , This involves Compose Gesture Used

3. Drag chess pieces , Trigger state change

Compose Event handling is also through Modifier Set up , for example Modifier.draggable(), Modifier.swipeable() You can use it out of the box . In the game scene of Huarong Road , have access to draggable Monitor drag

3.1 Monitor gestures

1) Use draggable Monitor gestures

Chess pieces can x Axis 、y Drag the axis in both directions , So we set two draggable

@Composable
fun ChessBoard (
    chessList: List<Chess>,
    onMove: (chess: String, x: Int, y: Int) -> Unit
) {
    Image(
        modifier = Modifier
           ...
           .draggable(// Monitor horizontal drag 
                 orientation = Orientation.Horizontal,
                 state = rememberDraggableState(onDelta = {
                     onMove(chess.name, it.roundToInt(), 0)
                 })
            )
            .draggable(// Monitor vertical drag 
                 orientation = Orientation.Vertical,
                 state = rememberDraggableState(onDelta = {
                     onMove(chess.name, 0, it.roundToInt())
                 })
            ),
            ...
    )
}

orientation Gesture used to specify which direction to listen for : Horizontal or vertical .rememberDraggableState Save drag state ,onDelta Specifies the callback of the gesture . We use custom onMove Throw out the displacement information of the drag gesture .

Now someone will ask ,draggable Can only monitor or drag horizontally or vertically , What if you want to monitor dragging in any direction , You can use detectDragGestures

2) Use pointerInput Monitor gestures

draggable , swipeable etc. , Its internal is through the call Modifier.pointerInput() Realized , be based on pointerInput You can implement more complex custom gestures :

fun Modifier.pointerInput(
    key1: Any?,
    block: suspend PointerInputScope.() -> Unit
) : Modifier = composed (...) {
    ...
}

pointerInput Provides PointerInputScope, You can use suspend Function to monitor various gestures . for example , have access to detectDragGestures Monitor dragging in any direction :

suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
)

detectDragGestures It also provides a level 、 Vertical versions are available , So in the scene of Huarong Road , The following methods can also be used for horizontal and vertical monitoring :

@Composable
fun ChessBoard (
    chessList: List<Chess>,
    onMove: (chess: String, x: Int, y: Int) -> Unit
) {
    Image(
        modifier = Modifier
            ...
            .pointerInput(Unit) {
                scope.launch {// Monitor horizontal drag 
                    detectHorizontalDragGestures { change, dragAmount ->
                        change.consumeAllChanges()
                        onMove(chess.name, 0, dragAmount.roundToInt())
                    }
                }
                scope.launch {// Monitor vertical drag 
                    detectVerticalDragGestures { change, dragAmount ->
                        change.consumeAllChanges()
                        onMove(chess.name, 0, dragAmount.roundToInt())
                    }
                }
            },
            ...
    )
}

We need to pay attention to detectHorizontalDragGestures and detectVerticalDragGestures It's a suspend function , Therefore, it is necessary to start the cooperation process separately for monitoring , It can be compared to multiple flow Of collect.

3.2 Collision detection of chess pieces

After obtaining the displacement information of pawn dragging , You can update the chess game status and finally refresh UI. However, before updating the status, it is necessary to detect the collision of chess pieces , The dragging of chess pieces has boundaries .

The principle of collision detection is simple : The piece cannot cross other pieces in the current moving direction .

1) Relative position determination

First , You need to determine the relative position between the pieces . You can use the following methods , Judge the pieces A On the chess pieces B On top of :

val Chess.left get() = offset.x
val Chess.right get() = left + width

infix fun Chess.isAboveOf(other: Chess) =
    (bottom <= other.top) && ((left until right) intersect (other.left until other.right)).isNotEmpty()

Disassemble the above conditional expression , namely pieces A The lower boundary of is located on the chess piece B Above the upper boundary And In the horizontal direction, the pieces A And chess pieces B There is an intersection of areas :

For example, in the chess game above , The following judgment results can be obtained :

  • Cao Cao be located Guan yu above
  • Guan yu be located pawn 1 Huang Zhong above
  • pawn 1 be located pawn 2 pawn 3 above

Although the position Guan yu be located pawn 2 On top of , But from the perspective of collision detection , Guan yu and pawn 2 stay x There is no intersection in the axis direction , therefore Guan yu stay y Movement in the axial direction will not collide with pawn 2,

guan.isAboveOf(zu1) == false

Empathy , Several other positional relationships are as follows :

infix fun Chess.isToRightOf(other: Chess) =
    (left >= other.right) && ((top until bottom) intersect (other.top until other.bottom)).isNotEmpty()

infix fun Chess.isToLeftOf(other: Chess) =
    (right <= other.left) && ((top until bottom) intersect (other.top until other.bottom)).isNotEmpty()

infix fun Chess.isBelowOf(other: Chess) =
    (top >= other.bottom) && ((left until right) intersect (other.left until other.right)).isNotEmpty()

2) Cross border detection

Next , Judge whether the chess piece crosses the boundary when moving , That is, whether it has crossed other pieces in its moving direction or out of bounds

for example , Where are the pieces x Check whether it is out of range during the movement in the axis direction :

// X Axial movement 
fun Chess.moveByX(x: Int) = moveBy(IntOffset(x, 0)) 

// Detect collisions and move fun Chess.checkAndMoveX(x: Int, others: List<Chess>): Chess {
    others.filter { it.name != name }.forEach { other ->
        if (x > 0 && this isToLeftOf other && right + x >= other.left)
            return moveByX(other.left - right)
        else if (x < 0 && this isToRightOf other && left + x <= other.right)
            return moveByX(other.right - left)
    }
    return if (x > 0) moveByX(min(x, boardWidth - right)) else moveByX(max(x, 0 - left))

}

The above logic is clear : When the pieces are x When the axis is moving positively , If it collides with the chess piece on its right, it stops moving ; Otherwise continue to move , Until it collides with the boundary of the chessboard , Other directions are the same .

3.3 Update chess game status

Sum up , After obtaining the gesture displacement information , Detect the collision and move to the correct position , Last update status , Refresh UI:

val chessList: List<Chess> by remember {
    mutableStateOf(opening.toList())
}                             

ChessBoard(chessList = chessState) { cur, x, y -> // onMove Callback 
         chessState = chessState.map { //it: Chess
            if (it.name == cur) {
                if (x != 0) it.checkAndMoveX(x, chessState)
                else it.checkAndMoveY(y, chessState)
            } else { it }
         }
}

4. Topic switching , Game skin change

Last , Let's see how to realize multiple sets of skin for the game , What is used is Compose Of Theme.

Compose Of Theme The configuration of is simple and intuitive , This is because it is based on CompositionLocal Realized . You can put CompositionLocal See it as a Composable Parent container of , It has two characteristics :

  1. His son Composable Can be Shared CompositionLocal Data in , Avoid layer by layer parameter transfer .
  2. When CompositionLocal When the data of , Son Composable Will automatically reorganize to get the latest data .

adopt CompositionLocal Characteristics , We can do that Compose Dynamic skin changing :

4.1 Define skin

First , We define multiple sets of skin , That is, multiple sets of picture resources of chess pieces

object DarkChess : ChessAssets {
    override val huangzhong = R.drawable.huangzhong
    override val caocao = R.drawable.caocao
    override val zhaoyun = R.drawable.zhaoyun
    override val zhangfei = R.drawable.zhangfei
    override val guanyu = R.drawable.guanyu
    override val machao = R.drawable.machao
    override val zu = R.drawable.zu
}

object LightChess : ChessAssets {
    //... ditto , A little 
}

object WoodChess : ChessAssets {
    //... ditto , A little 
}

4.2 establish CompositionLocal

Then create the skin CompositionLocal, We use compositionLocalOf Method creation

internal var LocalChessAssets = compositionLocalOf<ChessAssets> {
    DarkChess
}

Here DarkChess Is the default value , But it is not usually used directly , Generally we will pass CompositionLocalProvider by CompositionLocal establish Composable Containers , At the same time, set the current value :

CompositionLocalProvider(LocalChessAssets provides chess) {
     //...
 }

Its inner son Composable Share the value of the current setting .

4.3 Follow Theme Change switch skin

In this game , We hope to add the skin of chess pieces to the whole game theme , And follow Theme Change and switch :

@Composable
fun ComposehuarongdaoTheme(
    theme: Int = 0,
    content: @Composable() () -> Unit
) {
   
    val (colors, chess) = when (theme) {
        0 -> DarkColorPalette to DarkChess
        1 -> LightColorPalette to LightChess
        2 -> WoodColorPalette to WoodChess
        else -> error("")
    }

    CompositionLocalProvider(LocalChessAssets provides chess) {
        MaterialTheme(
            colors = colors,
            typography = Typography,
            shapes = Shapes,
            content = content
        )
    }

}

Definition theme The enumerated values , Get different values according to enumeration colors as well as ChessAssets, take MaterialTheme in LocalChessAssets Inside ,MaterialTheme In all of the Composalbe Can be Shared MaterialTheme and LocalChessAssets Value .

Last , by LocalChessAssets Order one MaterialTheme The extension function of ,

val MaterialTheme.chessAssets
    @Composable
    @ReadOnlyComposable
    get() = LocalChessAssets.current

Can be like visiting MaterialTheme Other properties of , visit ChessAssets.

Last

This article mainly introduces how to use Compose Of Gesture, Theme And other features to quickly complete a Huarong Road game , more API Implementation principle of , You can refer to the following article :

In depth understanding of MaterialTheme And CompositionLocal

Use Jetpack Compose Complete custom gesture processing

Code address :https://github.com/vitaviva/compose-huarongdao

At the end of the article What's your opinion on the article , Or any technical problems , Welcome to leave a message and discuss in the comment area !

原网站

版权声明
本文为[User 9239674]所创,转载请带上原文链接,感谢
https://yzsam.com/2021/12/202112131304370903.html