App Inventor 2 台球游戏制作教程

« 返回首页

概述

台球游戏的核心是2D物理模拟:碰撞检测、反弹计算、摩擦力。App Inventor的Canvas + ImageSprite可以实现。

已有文档canvas_game.md 介绍了Canvas游戏基础。本文聚焦台球物理。

核心物理概念

概念 说明 实现方式
碰撞检测 球与球/球与边界 距离计算
弹性碰撞 球碰撞后速度交换 向量计算
反弹 球碰到边界反弹 速度分量取反
摩擦力 球逐渐减速 速度衰减
入袋检测 球进入袋口 距离判断

界面设计

Screen_Pool
├── Canvas_球桌(球台画布)
│   ├── ImageSprite_白球
│   ├── ImageSprite_球1 ~ 球15
│   └── 球杆线(Canvas画线)
├── 按钮_击球
├── 标签_分数
├── 标签_状态
├── 滑块_力度(控制击球力量)
├── Clock_物理引擎(驱动物理模拟)
└── Clock_绘图(绘制辅助线)

球桌布局

摩擦系数

设 球台宽度 = Canvas_球桌.宽度
设 球台高度 = Canvas_球桌.高度
设 球半径 = 15
设 摩擦系数 = 0.995  ' 每帧速度保留99.5%
设 袋口半径 = 25
设 袋口位置 = [
  [0, 0],                           ' 左上
  [球台宽度/2, 0],                   ' 中上
  [球台宽度, 0],                     ' 右上
  [0, 球台高度],                     ' 左下
  [球台宽度/2, 球台高度],             ' 中下
  [球台宽度, 球台高度]               ' 右下
]

物理引擎

球的数据结构

设 球列表 = 创建空列表

定义 创建球(x, y, 颜色, 编号) 返回 球
  返回 创建字典(
    "x" → x, "y" → y,
    "vx" → 0, "vy" → 0,  ' 速度
    "radius" → 球半径,
    "color" → 颜色,
    "number" → 编号,
    "active" → true
  )

物理更新(每帧)

当 Clock_物理.计时
  设 i = 1
  当 i ≤ 列表长度(球列表)
    设 球 = 列表第i项(球列表)
    如果 从字典 球 获取 "active"
      ' 更新位置
      设 x = (从字典 球 获取 "x") + (从字典 球 获取 "vx")
      设 y = (从字典 球 获取 "y") + (从字典 球 获取 "vy")
      
      ' 摩擦力(速度衰减)
      设 vx = (从字典 球 获取 "vx") * 摩擦系数
      设 vy = (从字典 球 获取 "vy") * 摩擦系数
      
      ' 速度过小则停止
      如果 数学.绝对值(vx) < 0.05 且 数学.绝对值(vy) < 0.05
        设 vx = 0
        设 vy = 0
      
      ' 边界反弹
      如果 x - 球半径 < 0
        设 x = 球半径
        设 vx = -vx * 0.8  ' 反弹衰减
      否则 如果 x + 球半径 > 球台宽度
        设 x = 球台宽度 - 球半径
        设 vx = -vx * 0.8
      
      如果 y - 球半径 < 0
        设 y = 球半径
        设 vy = -vy * 0.8
      否则 如果 y + 球半径 > 球台高度
        设 y = 球台高度 - 球半径
        设 vy = -vy * 0.8
      
      ' 更新字典
      设 球 = 创建字典("x"→x, "y"→y, "vx"→vx, "vy"→vy, "radius"→球半径, "color"→从字典球获取"color", "number"→从字典球获取"number", "active"→true)
      设 列表第i项(球列表) = 球
      
      ' 入袋检测
      调用 检查入袋(球, i)
    设 i = i + 1
  
  ' 碰撞检测
  调用 碰撞检测()
  
  ' 重绘
  调用 绘制球桌()

球与球碰撞

定义 碰撞检测()
  设 i = 1
  当 i ≤ 列表长度(球列表)
    如果 从字典 列表第i项(球列表) 获取 "active"
      设 j = i + 1
      当 j ≤ 列表长度(球列表)
        如果 从字典 列表第j项(球列表) 获取 "active"
          调用 检查两球碰撞(i, j)
        设 j = j + 1
    设 i = i + 1

定义 检查两球碰撞(idx1, idx2)
  设 球1 = 列表第idx1项(球列表)
  设 球2 = 列表第idx2项(球列表)
  设 dx = (从字典 球2 获取 "x") - (从字典 球1 获取 "x")
  设 dy = (从字典 球2 获取 "y") - (从字典 球1 获取 "y")
  设 距离 = 数学.平方根(dx*dx + dy*dy)
  设 最小距离 = 2 * 球半径
  
  如果 距离 < 最小距离 且 距离 > 0
    ' 碰撞法向量
    设 nx = dx / 距离
    设 ny = dy / 距离
    
    ' 相对速度
    设 dvx = (从字典 球1 获取 "vx") - (从字典 球2 获取 "vx")
   设 dvy = (从字典 球1 获取 "vy") - (从字典 球2 获取 "vy")
    
    ' 法向速度分量
    设 dvn = dvx * nx + dvy * ny
    
    如果 dvn > 0  ' 只处理正在接近的球
      ' 等质量弹性碰撞:交换法向速度分量
      设 v1x = (从字典 球1 获取 "vx") - dvn * nx
      设 v1y = (从字典 球1 获取 "vy") - dvn * ny
      设 v2x = (从字典 球2 获取 "vx") + dvn * nx
      设 v2y = (从字典 球2 获取 "vy") + dvn * ny
      
      ' 更新速度
      设 新球1 = 设字典值(球1, "vx", v1x * 0.95)
      设 新球1 = 设字典值(新球1, "vy", v1y * 0.95)
      设 新球2 = 设字典值(球2, "vx", v2x * 0.95)
      设 新球2 = 设字典值(新球2, "vy", v2y * 0.95)
      设 列表第idx1项(球列表) = 新球1
      设 列表第idx2项(球列表) = 新球2
      
      ' 分离重叠的球
      设 重叠 = 最小距离 - 距离
      设 新球1 = 设字典值(新球1, "x", (从字典新球1获取"x") - 重叠/2*nx)
      设 新球1 = 设字典值(新球1, "y", (从字典新球1获取"y") - 重叠/2*ny)
      设 新球2 = 设字典值(新球2, "x", (从字典新球2获取"x") + 重叠/2*nx)
      设 新球2 = 设字典值(新球2, "y", (从字典新球2获取"y") + 重叠/2*ny)

入袋检测

定义 检查入袋(球, 索引)
  设 bx = 从字典 球 获取 "x"
  设 by = 从字典 球 获取 "y"
  设 i = 1
  当 i ≤ 列表长度(袋口位置)
    设 袋口 = 列表第i项(袋口位置)
    设 px = 从字典 袋口 获取 "x"
    设 py = 从字典 袋口 获取 "y"
    设 距离 = 数学.平方根((bx-px)^2 + (by-py)^2)
    如果 距离 < 袋口半径
      ' 入袋!
      设 球 = 设字典值(球, "active", false)
      设 列表第索引项(球列表) = 球
      设 分数 = 分数 + 10
      标签_分数.文本 = "分数:" & 分数
      如果 从字典 球 获取 "number" = 0
        标签_状态.文本 = "白球入袋!犯规"
      设 i = 列表长度(袋口位置) + 1  ' 跳出
    设 i = i + 1

击球控制

拖动瞄准

当 Canvas_球桌.拖动(被拖动, 起X, 起Y, 当X, 当Y, 上X, 上Y)
  如果 被拖动 且 等待击球
    ' 从白球到拖动位置画瞄准线
    设 白球 = 列表第1项(球列表)  ' 假设第1个是白球
    设 bx = 从字典 白球 获取 "x"
    设 by = 从字典 白球 获取 "y"
    调用 Canvas_球桌.画线(bx, by, 当X, 当Y)

当 Canvas_球桌.触摸释放(上X, 上Y)
  设 白球 = 列表第1项(球列表)
  设 bx = 从字典 白球 获取 "x"
  设 by = 从字典 白球 获取 "y"
  设 dx = bx - 上X  ' 反方向
  设 dy = by - 上Y
  设 距离 = 数学.平方根(dx*dx + dy*dy)
  设 力度 = 滑块_力度.当前位置 / 100 * 20  ' 最大速度20
  如果 距离 > 0
    设 vx = dx / 距离 * 力度
    设 vy = dy / 距离 * 力度
    设 新白球 = 设字典值(白球, "vx", vx)
    设 新白球 = 设字典值(新白球, "vy", vy)
    设 列表第1项(球列表) = 新白球

绘制

定义 绘制球桌()
  调用 Canvas_球桌.清除()
  ' 绘制袋口
  设 i = 1
  当 i ≤ 列表长度(袋口位置)
    设 p = 列表第i项(袋口位置)
    调用 Canvas_球桌.画圆(从字典p获取"x", 从字典p获取"y", 袋口半径, 黑色)
    设 i = i + 1
  ' 绘制球
  设 i = 1
  当 i ≤ 列表长度(球列表)
    设 球 = 列表第i项(球列表)
    如果 从字典 球 获取 "active"
      调用 Canvas_球桌.画圆(
        从字典球获取"x", 从字典球获取"y",
        球半径, 从字典球获取"color")
      ' 显示球号
      调用 Canvas_球桌.画文字(
        从字典球获取"x"-5, 从字典球获取"y"+5,
        从字典球获取"number", 白色)
    设 i = i + 1

常见问题

Q1: 碰撞后球会卡住?

确保碰撞后有分离步骤(将重叠的球推开)。

Q2: 性能卡顿?

  • 减少Clock频率(16ms → 33ms)
  • 球数量多时碰撞检测慢(O(n²)),可优化空间分区

Q3: 球运动不自然?

调整摩擦系数和反弹衰减系数,多测试找到合适参数。

总结

核心技术 关键
碰撞检测 距离 < 两球半径之和
弹性碰撞 交换法向速度分量
摩擦力 每帧速度乘以衰减系数
入袋 球心到袋口距离 < 袋口半径
击球 反方向拖动,力度控制速度

版权声明:MIT App Inventor 官方文档采用 CC BY-SA 4.0 授权,本文档由 ai2claw 🐝 整理。

文档反馈