Jetpack Compose中如何检测两个Composable视图的碰撞?
Jetpack Compose 2D游戏碰撞检测实现方案
核心思路
通过共享游戏状态统一管理箭头、气球的实时位置与尺寸,结合onGloballyPositioned获取布局坐标,在状态更新时检测两者的区域交集,触发得分逻辑。
具体实现步骤
- 提取共享状态:将游戏状态、得分、箭头/气球的位置尺寸状态提升到父Composable,确保Balloon和ShootBalloon能共享数据。
- 获取实时布局信息:在Balloon和箭头的可组合项中,用
onGloballyPositioned监听布局变化,将实时坐标(相对于根布局)和尺寸保存到共享状态。 - 碰撞检测逻辑:用
LaunchedEffect监听位置状态变化,定期检查两者是否相交。气球是圆形,采用圆形碰撞检测(更贴合视觉),箭头用矩形边界简化计算。 - 处理碰撞结果:检测到碰撞时更新得分,可按需重置箭头位置或触发气球消失动画。
修改后的代码示例
1. 定义数据类与游戏状态
enum class GameState { START, PLAYING } // 存储元素的位置与尺寸数据 data class ElementBounds( val left: Float, val top: Float, val right: Float, val bottom: Float, val centerX: Float, val centerY: Float, val radius: Float = 0f )
2. 父容器Composable(统一管理状态)
@Composable fun GameScreen() { val localConfig = LocalConfiguration.current var gameState by remember { mutableStateOf(GameState.START) } var score by remember { mutableStateOf(0) } // 气球的边界数据 var balloonBounds by remember { mutableStateOf(ElementBounds(0f, 0f, 0f, 0f, 0f, 0f)) } // 箭头的边界数据 var arrowBounds by remember { mutableStateOf(ElementBounds(0f, 0f, 0f, 0f, 0f, 0f)) } // 碰撞检测逻辑:监听位置变化,定期检查 LaunchedEffect(balloonBounds, arrowBounds, gameState) { if (gameState != GameState.PLAYING) return@LaunchedEffect // 约60帧/秒的频率检测,平衡精度与性能 while (true) { delay(16) val isCollided = checkCircleRectCollision(balloonBounds, arrowBounds) if (isCollided) { score++ // 可选:碰撞后重置箭头动画或调整气球位置,避免重复触发 } } } Column(modifier = Modifier.fillMaxSize()) { Text(text = "得分: $score", modifier = Modifier.padding(16.dp)) Balloon( modifier = Modifier.fillMaxWidth(), gameState = gameState, onBoundsUpdated = { bounds -> balloonBounds = bounds } ) ShootBalloon( modifier = Modifier.fillMaxWidth(), gameState = gameState, onGameStateChange = { gameState = it }, onArrowBoundsUpdated = { bounds -> arrowBounds = bounds } ) } } // 圆形(气球)与矩形(箭头)的碰撞检测函数 private fun checkCircleRectCollision(circle: ElementBounds, rect: ElementBounds): Boolean { // 计算矩形到圆心的最近点 val closestX = circle.centerX.coerceIn(rect.left, rect.right) val closestY = circle.centerY.coerceIn(rect.top, rect.bottom) // 计算最近点到圆心的距离平方 val distanceX = circle.centerX - closestX val distanceY = circle.centerY - closestY val distanceSquared = distanceX * distanceX + distanceY * distanceY // 距离平方小于等于半径平方则判定碰撞 return distanceSquared <= circle.radius * circle.radius }
3. 修改Balloon Composable(添加边界回调)
@Composable fun Balloon( modifier: Modifier, gameState: GameState, onBoundsUpdated: (ElementBounds) -> Unit ) { val localConfig = LocalConfiguration.current val offsetX by animateDpAsState( targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 80.dp), animationSpec = infiniteRepeatable( animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing), repeatMode = RepeatMode.Reverse ) ) val offsetY by animateDpAsState( targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenHeightDp.dp / 3), animationSpec = infiniteRepeatable( animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing), repeatMode = RepeatMode.Reverse ) ) Box( modifier = modifier .offset(offsetX, offsetY) .size(80.dp) .background(Color.Red, shape = CircleShape) .onGloballyPositioned { layoutCoordinates -> val position = layoutCoordinates.positionInRoot() val size = layoutCoordinates.size val centerX = position.x + size.width / 2f val centerY = position.y + size.height / 2f onBoundsUpdated( ElementBounds( left = position.x, top = position.y, right = position.x + size.width, bottom = position.y + size.height, centerX = centerX, centerY = centerY, radius = size.width / 2f // 气球为圆形,半径取宽度的一半 ) ) }, contentAlignment = Alignment.Center ) { Text(text = "D!!") } }
4. 修改ShootBalloon Composable(添加箭头边界回调)
@Composable fun ShootBalloon( modifier: Modifier, gameState: GameState, onGameStateChange: (GameState) -> Unit, onArrowBoundsUpdated: (ElementBounds) -> Unit ) { val localConfig = LocalConfiguration.current val offsetX by animateDpAsState( targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 50.dp), animationSpec = infiniteRepeatable( animation = tween(2000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ) ) val offsetY by animateDpAsState( targetValue = if (gameState == GameState.START) 0.dp else (-(localConfig.screenHeightDp.dp - 50.dp)), animationSpec = infiniteRepeatable( animation = tween(500, easing = LinearEasing), repeatMode = RepeatMode.Restart ) ) Column( modifier = modifier, verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.Start ) { if (gameState == GameState.START) { Button(onClick = { onGameStateChange(GameState.PLAYING) }) { Text(text = "Play") } } Box( modifier = Modifier .size(50.dp) .offset(x = offsetX) .background(Color.Green), contentAlignment = Alignment.Center ) { Image( painter = painterResource(R.drawable.ic_baseline_arrow_upward_24), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier .offset(y = offsetY) .onGloballyPositioned { layoutCoordinates -> val position = layoutCoordinates.positionInRoot() val size = layoutCoordinates.size onArrowBoundsUpdated( ElementBounds( left = position.x, top = position.y, right = position.x + size.width, bottom = position.y + size.height, centerX = position.x + size.width / 2f, centerY = position.y + size.height / 2f ) ) } ) } } }
关键注意事项
- 状态提升:必须将碰撞相关状态提升到共同父容器,避免子Composable间状态隔离导致无法共享位置数据。
- 碰撞精度:圆形碰撞更贴合气球视觉形状,比矩形检测更准确;若箭头是不规则图形,可进一步调整碰撞区域。
- 性能优化:用
delay(16)控制检测频率,避免过于频繁的计算消耗性能。 - 碰撞后处理:碰撞后可重置箭头动画或调整气球位置,避免重复触发碰撞检测。
内容的提问来源于stack exchange,提问作者iCantC
相关产品推荐
相关产品推荐

