引言
抓娃娃机是游乐场和商场中备受欢迎的娱乐设备,玩家通过操控机械臂移动、下爪、抓取,试图将心仪的玩偶从玻璃箱中取出。这一过程看似简单,实则涉及精密的运动规划与碰撞检测:机械臂如何在三维空间中避开箱体和障碍物到达目标正上方?爪子下降时如何确保不与玩偶或箱体发生碰撞?本文将用Java实现一个简化版二维抓娃娃机模拟器,重点讲解关节型机械臂逆运动学求解与圆形碰撞检测两大核心算法。
核心算法原理
1. 关节型机械臂逆运动学(IK)
机械臂由多个连杆和旋转关节组成。逆运动学的目标是:给定末端执行器(爪子)的目标位置 (tx, ty),求解各关节的旋转角度。
对于两关节机械臂(臂长分别为 L1 和 L2):
– 设基座到目标的距离为 D = sqrt(tx² + ty²)
– 使用余弦定理求解第二关节角度 θ2:
cos(θ2) = (D² - L1² - L2²) / (2 * L1 * L2)
θ2 = arccos(cos(θ2))
– 再求解第一关节角度 θ1:
θ1 = atan2(ty, tx) - atan2(L2*sin(θ2), L1 + L2*cos(θ2))
2. 圆形碰撞检测
在二维投影中,爪子和玩偶均可近似为圆形。碰撞检测即判断两圆圆心距离是否小于半径之和:
碰撞 = sqrt((x1-x2)² + (y1-y2)²) < (r1 + r2)
若检测到碰撞,则禁止该方向的运动,或触发抓取逻辑。
Java完整实现
import java.util.ArrayList;
import java.util.List;
/**
* 简化版抓娃娃机模拟器
* 核心算法:关节型机械臂逆运动学 + 圆形碰撞检测
*/
public class ClawMachineSimulator {
// 机械臂参数
private static final double ARM_L1 = 80; // 第一臂长
private static final double ARM_L2 = 60; // 第二臂长
private static final double CLAW_SIZE = 8; // 爪子半径
// 玻璃箱边界
private static final double BOX_LEFT = 50;
private static final double BOX_RIGHT = 250;
private static final double BOX_TOP = 80;
private static final double BOX_BOTTOM = 220;
// 机械臂状态
private double baseX = 150, baseY = 30; // 基座位置
private double theta1 = Math.toRadians(90); // 第一关节角度
private double theta2 = Math.toRadians(0); // 第二关节角度
private double clawOpen = 1.0; // 爪子开合度 (0=闭合, 1=张开)
private boolean isGrabbing = false;
// 关节坐标
private double joint1X, joint1Y; // 第一关节
private double joint2X, joint2Y; // 末端/爪子中心
// 玩偶列表
static class Doll {
double x, y, radius;
boolean isGrabbed;
String name;
Doll(double x, double y, double radius, String name) {
this.x = x; this.y = y;
this.radius = radius;
this.name = name;
this.isGrabbed = false;
}
}
private final List<Doll> dolls;
public ClawMachineSimulator() {
this.dolls = new ArrayList<>();
initDolls();
updateForwardKinematics();
}
private void initDolls() {
dolls.add(new Doll(100, 180, 12, "小熊"));
dolls.add(new Doll(140, 190, 10, "兔子"));
dolls.add(new Doll(180, 170, 14, "企鹅"));
dolls.add(new Doll(120, 150, 9, "猫咪"));
dolls.add(new Doll(200, 185, 11, "狗狗"));
}
/**
* 核心算法1:正向运动学
* 根据关节角度计算各关节坐标
*/
public void updateForwardKinematics() {
joint1X = baseX + ARM_L1 * Math.cos(theta1);
joint1Y = baseY + ARM_L1 * Math.sin(theta1);
joint2X = joint1X + ARM_L2 * Math.cos(theta1 + theta2);
joint2Y = joint1Y + ARM_L2 * Math.sin(theta1 + theta2);
}
/**
* 核心算法2:逆运动学求解
* 给定目标位置,求解关节角度
* @param targetX 目标x坐标
* @param targetY 目标y坐标
* @return 是否可达
*/
public boolean solveInverseKinematics(double targetX, double targetY) {
double dx = targetX - baseX;
double dy = targetY - baseY;
double D = Math.sqrt(dx * dx + dy * dy);
// 检查可达性
if (D > ARM_L1 + ARM_L2 || D < Math.abs(ARM_L1 - ARM_L2)) {
return false; // 目标超出工作空间
}
// 余弦定理求解 theta2
double cosTheta2 = (D * D - ARM_L1 * ARM_L1 - ARM_L2 * ARM_L2)
/ (2 * ARM_L1 * ARM_L2);
// 限制在 [-1, 1] 范围内避免浮点误差
cosTheta2 = Math.max(-1.0, Math.min(1.0, cosTheta2));
double theta2New = Math.acos(cosTheta2);
// 选择肘部向下的解(更自然)
// theta2New = -theta2New; // 可切换为另一种构型
// 求解 theta1
double alpha = Math.atan2(dy, dx);
double beta = Math.atan2(ARM_L2 * Math.sin(theta2New),
ARM_L1 + ARM_L2 * Math.cos(theta2New));
double theta1New = alpha - beta;
// 关节角度限制(避免机械结构冲突)
if (theta1New < Math.toRadians(10) || theta1New > Math.toRadians(170)) {
return false;
}
this.theta1 = theta1New;
this.theta2 = theta2New;
updateForwardKinematics();
return true;
}
/**
* 核心算法3:圆形碰撞检测
* @return 碰撞的玩偶,无碰撞返回null
*/
public Doll checkCollision() {
for (Doll doll : dolls) {
double dx = joint2X - doll.x;
double dy = joint2Y - doll.y;
double dist = Math.sqrt(dx * dx + dy * dy);
if (dist < CLAW_SIZE + doll.radius) {
return doll;
}
}
return null;
}
/**
* 检测爪子是否与箱体边界碰撞
*/
public boolean checkBoxCollision() {
return joint2X - CLAW_SIZE < BOX_LEFT ||
joint2X + CLAW_SIZE > BOX_RIGHT ||
joint2Y - CLAW_SIZE < BOX_TOP ||
joint2Y + CLAW_SIZE > BOX_BOTTOM;
}
/**
* 移动爪子到目标位置(带碰撞检测的安全移动)
*/
public boolean moveClawTo(double targetX, double targetY) {
// 目标限制在箱体内
targetX = Math.max(BOX_LEFT + CLAW_SIZE,
Math.min(BOX_RIGHT - CLAW_SIZE, targetX));
targetY = Math.max(BOX_TOP + CLAW_SIZE,
Math.min(BOX_BOTTOM - CLAW_SIZE, targetY));
if (!solveInverseKinematics(targetX, targetY)) {
return false; // 目标不可达
}
// 检测与箱体碰撞
if (checkBoxCollision()) {
return false;
}
return true;
}
/**
* 执行抓取操作
*/
public boolean grab() {
clawOpen = 0.0; // 闭合爪子
Doll hit = checkCollision();
if (hit != null) {
hit.isGrabbed = true;
isGrabbing = true;
return true;
}
return false;
}
/**
* 执行释放操作
*/
public void release() {
clawOpen = 1.0; // 张开爪子
for (Doll doll : dolls) {
if (doll.isGrabbed) {
doll.isGrabbed = false;
// 释放后玩偶落回底部
doll.y = Math.min(doll.y + 30, BOX_BOTTOM - doll.radius);
}
}
isGrabbing = false;
}
/**
* AI自动抓取策略
*/
public void aiAutoGrab() {
System.out.println("=== AI自动抓取 ===");
for (Doll doll : dolls) {
if (doll.isGrabbed) continue;
System.out.printf("尝试抓取: %s (位置: %.0f, %.0f)%n", doll.name, doll.x, doll.y);
// 步骤1:移动到玩偶正上方
double approachX = doll.x;
double approachY = doll.y - doll.radius - 5;
if (!moveClawTo(approachX, approachY)) {
System.out.println(" 正上方不可达,尝试偏移...");
// 尝试多个偏移位置
boolean success = false;
for (double offset = -20; offset <= 20; offset += 10) {
if (moveClawTo(approachX + offset, approachY)) {
success = true;
break;
}
}
if (!success) {
System.out.println(" 无法到达该玩偶附近,跳过");
continue;
}
}
// 步骤2:下降
System.out.println(" 下降中...");
for (double y = approachY; y <= doll.y; y += 3) {
if (!moveClawTo(joint2X, y)) {
System.out.println(" 下降路径被阻挡");
break;
}
render();
sleep(100);
}
// 步骤3:抓取
if (grab()) {
System.out.println(" ✓ 抓取成功!");
} else {
System.out.println(" ✗ 抓取失败,未命中");
release();
continue;
}
// 步骤4:提起并移出
System.out.println(" 提起并移出...");
for (double y = joint2Y; y >= BOX_TOP - 20; y -= 5) {
moveClawTo(joint2X, y);
// 同步移动被抓取的玩偶
for (Doll d : dolls) {
if (d.isGrabbed) {
d.x = joint2X;
d.y = joint2Y;
}
}
render();
sleep(100);
}
// 步骤5:释放(成功取出)
release();
System.out.println(" ✓ " + doll.name + " 成功取出!");
break; // 每次只抓一个
}
}
private void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {}
}
/**
* 控制台可视化
*/
public void render() {
int W = 60, H = 24;
char[][] canvas = new char[H][W];
for (char[] row : canvas) java.util.Arrays.fill(row, ' ');
// 绘制箱体
int bl = (int)(BOX_LEFT / 5), br = (int)(BOX_RIGHT / 5);
int bt = (int)(BOX_TOP / 10), bb = (int)(BOX_BOTTOM / 10);
for (int x = bl; x <= br; x++) {
canvas[bt][x] = '─'; canvas[bb][x] = '─';
}
for (int y = bt; y <= bb; y++) {
canvas[y][bl] = '│'; canvas[y][br] = '│';
}
canvas[bt][bl] = '┌'; canvas[bt][br] = '┐';
canvas[bb][bl] = '└'; canvas[bb][br] = '┘';
// 绘制玩偶
for (Doll doll : dolls) {
int dx = (int)(doll.x / 5);
int dy = (int)(doll.y / 10);
if (dx >= 0 && dx < W && dy >= 0 && dy < H) {
canvas[dy][dx] = doll.isGrabbed ? '*' : 'o';
}
}
// 绘制机械臂
int bx = (int)(baseX / 5), by = (int)(baseY / 10);
int j1x = (int)(joint1X / 5), j1y = (int)(joint1Y / 10);
int j2x = (int)(joint2X / 5), j2y = (int)(joint2Y / 10);
drawLine(canvas, bx, by, j1x, j1y, '=');
drawLine(canvas, j1x, j1y, j2x, j2y, '-');
if (j2x >= 0 && j2x < W && j2y >= 0 && j2y < H) {
canvas[j2y][j2x] = clawOpen > 0.5 ? 'C' : 'c'; // C=张开, c=闭合
}
System.out.print("\033[H\033[2J");
for (char[] row : canvas) System.out.println(new String(row));
System.out.printf("关节角度: θ1=%.1f° θ2=%.1f° | 爪子: %s%n",
Math.toDegrees(theta1), Math.toDegrees(theta2),
clawOpen > 0.5 ? "张开" : "闭合");
}
private void drawLine(char[][] canvas, int x1, int y1, int x2, int y2, char ch) {
int steps = Math.max(Math.abs(x2-x1), Math.abs(y2-y1));
if (steps == 0) return;
for (int i = 0; i <= steps; i++) {
int x = x1 + (x2-x1) * i / steps;
int y = y1 + (y2-y1) * i / steps;
if (x >= 0 && x < canvas[0].length && y >= 0 && y < canvas.length) {
canvas[y][x] = ch;
}
}
}
public static void main(String[] args) {
ClawMachineSimulator sim = new ClawMachineSimulator();
System.out.println("=== 抓娃娃机模拟器 ===");
System.out.println("o = 玩偶, * = 被抓取的玩偶, C/c = 爪子, =/- = 机械臂");
sim.render();
sim.sleep(1000);
sim.aiAutoGrab();
System.out.println("\n模拟结束!");
System.out.println("成功抓取的玩偶:");
for (Doll d : sim.dolls) {
System.out.printf(" %s: %s%n", d.name, d.isGrabbed ? "已抓取(等待释放)" : "仍在箱中");
}
}
}
算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 逆运动学求解 | O(1) | O(1) | 固定公式计算 |
| 正向运动学 | O(1) | O(1) | 三角函数运算 |
| 圆形碰撞检测 | O(N) | O(1) | N为玩偶数量 |
| 安全移动(含碰撞检测) | O(N) | O(1) | 每步检测一次 |
| AI自动抓取 | O(N × M) | O(1) | N为玩偶数,M为偏移尝试次数 |
总结
本文通过Java实现了一个简化版抓娃娃机模拟器,重点实现了两个核心算法:
-
关节型机械臂逆运动学(IK):利用余弦定理和三角函数关系,将二维目标位置反向求解为两个关节的旋转角度。通过可达性检查和工作空间限制,确保机械臂运动在安全范围内。
-
圆形碰撞检测:在二维投影中将爪子和玩偶建模为圆形,通过圆心距与半径和的比较实现高效碰撞判定。结合箱体边界检测,构建了完整的安全运动约束系统。
这两个算法是工业机器人控制和游戏物理交互的基础。逆运动学的数学思想广泛应用于机械臂轨迹规划、角色动画骨骼系统和三维建模中;圆形碰撞检测则是2D游戏物理引擎中最基础的碰撞原语。理解这些原理,对掌握计算机图形学和机器人学都有重要价值。