基本思路
游戏逻辑比较简单,所以没有运用 MVI 之类的结构,可是全体仍然遵从数据驱动UI的设计思想:
- 界说游戏的状况
- 依据状况的UI制作
- 用户输入触发状况改变
1. 界说游戏状况
游戏的状况很简单,即当时各棋子(Chees)的摆放方位,所以能够将一个棋子的 List 作为承载 State 的数据结构
1.1 棋子界说
先来看一下单个棋子的界说
data class Chess(
val name: String, //人物名称
val drawable: Int //人物图片
val w: Int, //棋子宽度
val h: Int, //棋子长度
val offset: IntOffset = IntOffset(0, 0) //偏移量
)
经过 w
,h
能够确认棋子的形状,offset
确认在棋牌中的当时方位
1.2 局面棋子摆放
接下来咱们界说各个人物的棋子,并按照局面的状况摆放这些棋子
val zhang = Chess("张飞", R.drawable.zhangfei, 1, 2)
val cao = Chess("曹操", R.drawable.caocao, 2, 2)
val huang = Chess("黄忠", R.drawable.huangzhong, 1, 2)
val zhao = Chess("赵云", R.drawable.zhaoyun, 1, 2)
val ma = Chess("马超", R.drawable.machao, 1, 2)
val guan = Chess("关羽", R.drawable.guanyu, 2, 1)
val zu = buildList { repeat(4) { add(Chess("卒$it", R.drawable.zu, 1, 1)) } }
各人物的界说中明确棋子形状,比如“张飞”的长宽比是 2:1,“曹操” 的长宽比是2:2。
接下来界说一个游戏局面:
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
的三个成员别离表明棋子以及其在棋盘中的偏移,例如 Triple(cao, 1, 0)
表明曹操局面处于(1,0)坐标。
最终经过下面代码,将 gameOpening
转化为咱们所需的 State, 即一个 List<Chess>
:
const val boardGridPx = 200 //棋子单位尺寸
fun ChessOpening.toList() =
map { (chess, x, y) ->
chess.moveBy(IntOffset(x * boardGridPx, y * boardGridPx))
}
2. UI烘托,制作棋局
有了 List<Chess>
之后,依次制作棋子,然后完结整个棋局的制作。
@Composable
fun ChessBoard (chessList: List<Chess>) {
Box(
Modifier
.width(boardWidth.toDp())
.height(boardHeight.toDp())
) {
chessList.forEach { chess ->
Image( //棋子图片
Modifier
.offset { chess.offset } //偏移方位
.width(chess.width.toDp()) //棋子宽度
.height(chess.height.toDp())) //棋子高度
painter = painterResource(id = chess.drawable),
contentDescription = chess.name
)
}
}
}
Box
确认棋盘的规模,Image
制作棋子,并经过 Modifier.offset{ }
将其摆放到正确的方位。
到此停止,咱们运用 Compose 制作了一个静态的局面,接下来便是让棋子跟从手指动起来,这就涉及到 Compose Gesture 的运用了
3. 拖拽棋子,触发状况改变
Compose 的事件处理也是经过 Modifier
设置的, 例如 Modifier.draggable()
, Modifier.swipeable()
等能够做到开箱即用。华容道的游戏场景中,能够运用 draggable
监听拖拽
3.1 监听手势
1) 运用 draggable 监听手势
棋子能够x轴、y轴两个方向进行拖拽,所以咱们别离设置两个 draggable
:
@Composable
fun ChessBoard (
chessList: List<Chess>,
onMove: (chess: String, x: Int, y: Int) -> Unit
) {
Image(
modifier = Modifier
...
.draggable(//监听水平拖拽
orientation = Orientation.Horizontal,
state = rememberDraggableState(onDelta = {
onMove(chess.name, it.roundToInt(), 0)
})
)
.draggable(//监听笔直拖拽
orientation = Orientation.Vertical,
state = rememberDraggableState(onDelta = {
onMove(chess.name, 0, it.roundToInt())
})
),
...
)
}
orientation
用来指定监听什么方向的手势:水平或笔直。rememberDraggableState
保存拖动状况,onDelta
指定手势的回调。咱们经过自界说的 onMove
将拖拽手势的位移信息抛出。
此刻有人会问了,draggable
只能监听或许水平或许笔直的拖拽,那假如想监听恣意方向的拖拽呢,此刻能够运用 detectDragGestures
2) 运用 pointerInput 监听手势
draggable
, swipeable
等,其内部都是经过调用 Modifier.pointerInput()
完结的,依据 pointerInput
能够完结更杂乱的自界说手势:
fun Modifier.pointerInput(
key1: Any?,
block: suspend PointerInputScope.() -> Unit
) : Modifier = composed (...) {
...
}
pointerInput
供给了 PointerInputScope
,在其间能够运用suspend函数对各种手势进行监听。例如,能够运用 detectDragGestures
监听恣意方向的拖拽:
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
)
detectDragGestures
也供给了水平、笔直版别供挑选,所以在华容道的场景中,也能够运用以下方式进行水平和笔直方向的监听:
@Composable
fun ChessBoard (
chessList: List<Chess>,
onMove: (chess: String, x: Int, y: Int) -> Unit
) {
Image(
modifier = Modifier
...
.pointerInput(Unit) {
scope.launch {//监听水平拖拽
detectHorizontalDragGestures { change, dragAmount ->
change.consumeAllChanges()
onMove(chess.name, 0, dragAmount.roundToInt())
}
}
scope.launch {//监听笔直拖拽
detectVerticalDragGestures { change, dragAmount ->
change.consumeAllChanges()
onMove(chess.name, 0, dragAmount.roundToInt())
}
}
},
...
)
}
需求留意 detectHorizontalDragGestures
和 detectVerticalDragGestures
是挂起函数,所以需求别离启动协程进行监听,能够类比成多个 flow
的 collect
。
3.2 棋子的磕碰检测
获取了棋子拖拽的位移信息后,能够更新棋局状况并终究改写UI。可是在更新状况之前需求对棋子的磕碰进行检测,棋子的拖拽是有鸿沟的。
磕碰检测的准则很简单:棋子不能越过当时移动方向上的其他棋子。
1) 相对方位判定
首要,需求确认棋子之间的相对方位。能够运用下面办法,判定棋子A在棋子B的上方:
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()
拆解上述条件表达式,即 棋子A的下鸿沟坐落棋子B上鸿沟之上
且 在水平方向上棋子A与棋子B的区域有交集
:
比如上面的棋局中,能够得到如下判定成果:
-
曹操
坐落关羽
之上 -
关羽
坐落卒1
黄忠
之上 -
卒1
坐落卒2
卒3
之上
虽然方位上 关羽
坐落卒2
的上方,可是从磕碰检测的视点看,关羽
和 卒2
在x轴方向没有交集,因而 关羽
在y轴方向上的移动不会磕碰到 卒2
,
guan.isAboveOf(zu1) == false
同理,其他几种方位关系如下:
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) 越界检测
接下来,判别棋子移动时是否越界,即是否越过了其移动方向上的其他棋子或许出界
例如,棋子在x轴方向的移动中查看是否越界:
// X轴方向移动
fun Chess.moveByX(x: Int) = moveBy(IntOffset(x, 0))
//检测磕碰并移动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))
}
上述逻辑很明晰:当棋子在x轴方向正移动时,假如磕碰到其右侧的棋子则中止移动;不然继续移动,直至磕碰棋盘鸿沟停止 ,其他方向同理。
3.3 更新棋局状况
综上,获取手势位移信息后,检测磕碰并移动到正确方位,最终更新状况,改写UI:
val chessList: List<Chess> by remember {
mutableStateOf(opening.toList())
}
ChessBoard(chessList = chessState) { cur, x, y -> // onMove回调
chessState = chessState.map { //it: Chess
if (it.name == cur) {
if (x != 0) it.checkAndMoveX(x, chessState)
else it.checkAndMoveY(y, chessState)
} else { it }
}
}
4. 主题切换,游戏换肤
最终,再来看一下怎么为游戏完结多套皮肤,用到的是 Compose 的 Theme。
Compose 的 Theme 的装备简单直观,这要得益于它是依据 CompositionLocal
完结的。能够把 CompositionLocal
看做是一个 Composable 的父容器,它有两个特点:
- 其子 Composable 能够共享 CompositionLocal 中的数据,避免了层层参数传递。
- 当
CompositionLocal
的数据发生改变时,子 Composable 会自动重组以获取最新数据。
经过 CompositionLocal
的特点,咱们能够完结 Compose 的动态换肤:
4.1 界说皮肤
首要,咱们界说多套皮肤,也便是棋子的多套图片资源
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 {
//...同上,略
}
object WoodChess : ChessAssets {
//...同上,略
}
4.2 创立 CompositionLocal
然后创立皮肤的 CompositionLocal
, 咱们运用 compositionLocalOf
办法创立
internal var LocalChessAssets = compositionLocalOf<ChessAssets> {
DarkChess
}
此处的 DarkChess
是默认值,但一般不会直接运用,一般咱们会经过 CompositionLocalProvider
为 CompositionLocal
创立 Composable 容器,同时设置当时值:
CompositionLocalProvider(LocalChessAssets provides chess) {
//...
}
其内部的子Composable共享当时设置的值。
4.3 跟从 Theme 改变切换皮肤
这个游戏中,咱们希望将棋子的皮肤加入到整个游戏主题中,并跟从 Theme 改变而切换:
@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
)
}
}
界说 theme 的枚举值, 依据枚举获取不同的 colors
以及 ChessAssets
, 将 MaterialTheme
置于 LocalChessAssets
内部,MaterialTheme
内的所有 Composalbe 能够共享 MaterialTheme 和 LocalChessAssets 的值。
最终,为 LocalChessAssets 定一个 MaterialTheme 的扩展函数,
val MaterialTheme.chessAssets
@Composable
@ReadOnlyComposable
get() = LocalChessAssets.current
能够像拜访 MaterialTheme 的其他特点相同,拜访 ChessAssets
。
最终
本文主要介绍了怎么运用 Compose 的 Gesture, Theme 等特性快速完结一个华容道小游戏,更多 API 的完结原理,能够参阅以下文章:
深化理解 MaterialTheme 与 CompositionLocal
运用Jetpack Compose完结自界说手势处理
代码地址:github.com/vitaviva/co…
文末
您的点赞保藏便是对我最大的鼓励! 欢迎重视我,分享Android干货,沟通Android技术。 对文章有何见解,或许有何技术问题,欢迎在谈论区一起留言评论!