You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 07:40:32