概述
台球游戏的核心是2D物理模拟:碰撞检测、反弹计算、摩擦力。App Inventor的Canvas + ImageSprite可以实现。
已有文档:Canvas游戏基础。本文聚焦台球物理。
核心物理概念
| 概念 | 说明 | 实现方式 |
|---|---|---|
| 碰撞检测 | 球与球/球与边界 | 距离计算 |
| 弹性碰撞 | 球碰撞后速度交换 | 向量计算 |
| 反弹 | 球碰到边界反弹 | 速度分量取反 |
| 摩擦力 | 球逐渐减速 | 速度衰减 |
| 入袋检测 | 球进入袋口 | 距离判断 |
界面设计
Screen_Pool
├── Canvas_球桌(球台画布)
│ ├── ImageSprite_白球
│ ├── ImageSprite_球1 ~ 球15
│ └── 球杆线(Canvas画线)
├── 按钮_击球
├── 标签_分数
├── 标签_状态
├── 滑块_力度(控制击球力量)
├── Clock_物理引擎(驱动物理模拟)
└── Clock_绘图(绘制辅助线)
球桌布局
摩擦系数
global 球台宽度 = 480
global 球台高度 = 800
global 球半径 = 15
global 摩擦系数 = 0.995
global 袋口半径 = 25
global 袋口X = [0, 240, 480, 0, 240, 480]
global 袋口Y = [0, 0, 0, 800, 800, 800]
物理引擎
球的数据结构
global 球X = []
global 球Y = []
global 球VX = []
global 球VY = []
global 球编号 = []
global 球激活 = []
procedure 创建球(x, y, 编号) {
listadd(球X, x)
listadd(球Y, y)
listadd(球VX, 0)
listadd(球VY, 0)
listadd(球编号, 编号)
listadd(球激活, true)
}
物理更新(每帧)
global 球X = []
global 球Y = []
global 球VX = []
global 球VY = []
global 球激活 = []
global 球半径 = 15
global 球台宽度 = 480
global 球台高度 = 800
global 摩擦系数 = 0.995
when 计时器_物理引擎.Timer() {
var i = 1
while i <= length(球X) {
if select(球激活, i) {
var x = select(球X, i) + select(球VX, i)
var y = select(球Y, i) + select(球VY, i)
var vx = select(球VX, i) * 摩擦系数
var vy = select(球VY, i) * 摩擦系数
if abs(vx) < 0.05 { vx = 0 }
if abs(vy) < 0.05 { vy = 0 }
if x - 球半径 < 0 { x = 球半径; vx = -vx * 0.8 }
if x + 球半径 > 球台宽度 { x = 球台宽度 - 球半径; vx = -vx * 0.8 }
if y - 球半径 < 0 { y = 球半径; vy = -vy * 0.8 }
if y + 球半径 > 球台高度 { y = 球台高度 - 球半径; vy = -vy * 0.8 }
listreplace(球X, i, x)
listreplace(球Y, i, y)
listreplace(球VX, i, vx)
listreplace(球VY, i, vy)
}
i = i + 1
}
碰撞检测()
绘制球桌()
}
球与球碰撞
global 球X = []
global 球Y = []
global 球VX = []
global 球VY = []
global 球半径 = 15
procedure 碰撞检测() {
for i = 1 to length(球X) {
for j = i + 1 to length(球X) {
var dx = select(球X, j) - select(球X, i)
var dy = select(球Y, j) - select(球Y, i)
var 距离 = sqrt(dx ^ 2 + dy ^ 2)
if 距离 < 2 * 球半径 and 距离 > 0 {
listreplace(球VX, i, select(球VX, j))
listreplace(球VX, j, select(球VX, i))
listreplace(球VY, i, select(球VY, j))
listreplace(球VY, j, select(球VY, i))
}
}
}
}
入袋检测
global 球X = []
global 球Y = []
global 球激活 = []
global 袋口X = [0, 240, 480, 0, 240, 480]
global 袋口Y = [0, 0, 0, 800, 800, 800]
global 袋口半径 = 25
global 分数 = 0
procedure 检查入袋(索引) {
for i = 1 to length(袋口X) {
var dx = select(球X, 索引) - select(袋口X, i)
var dy = select(球Y, 索引) - select(袋口Y, i)
if sqrt(dx ^ 2 + dy ^ 2) < 袋口半径 {
listreplace(球激活, 索引, false)
分数 = 分数 + 10
标签_分数.Text = join("分数:", 分数)
}
}
}
击球控制
拖动瞄准
global 球X = [100]
global 球Y = [400]
global 球VX = [0]
global 球VY = [0]
global 等待击球 = true
when 画布_球桌.Dragged(startX, startY, prevX, prevY, currentX, currentY, draggedAnySprite) {
if 等待击球 and draggedAnySprite {
画布_球桌.DrawLine(select(球X, 1), select(球Y, 1), currentX, currentY)
}
}
when 画布_球桌.TouchUp(x, y) {
var dx = select(球X, 1) - x
var dy = select(球Y, 1) - y
var 距离 = sqrt(dx ^ 2 + dy ^ 2)
if 距离 > 0 {
listreplace(球VX, 1, dx / 距离 * 20)
listreplace(球VY, 1, dy / 距离 * 20)
等待击球 = false
}
}
绘制
global 球X = []
global 球Y = []
global 球激活 = []
global 球半径 = 15
procedure 绘制球桌() {
画布_球桌.Clear()
for i = 1 to length(球X) {
if select(球激活, i) {
画布_球桌.DrawCircle(select(球X, i), select(球Y, i), 球半径, true)
}
}
}
常见问题
Q1: 碰撞后球会卡住?
确保碰撞后有分离步骤(将重叠的球推开)。
Q2: 性能卡顿?
- 减少Clock频率(16ms → 33ms)
- 球数量多时碰撞检测慢(O(n²)),可优化空间分区
Q3: 球运动不自然?
调整摩擦系数和反弹衰减系数,多测试找到合适参数。
总结
| 核心技术 | 关键 |
|---|---|
| 碰撞检测 | 距离 < 两球半径之和 |
| 弹性碰撞 | 交换法向速度分量 |
| 摩擦力 | 每帧速度乘以衰减系数 |
| 入袋 | 球心到袋口距离 < 袋口半径 |
| 击球 | 反方向拖动,力度控制速度 |
版权声明:MIT App Inventor 官方文档采用 CC BY-SA 4.0 授权,本文档由 ai2claw 🐝 整理。
扫码添加客服咨询