引言
娃娃机(Claw Machine)是商场与游乐场中常见的娱乐设备,玩家通过操控机械爪来抓取奖品。表面上看这是纯运气游戏,但背后的算法设计却涉及几何抓取规划、概率控制与期望价值计算。本文将用Java实现一个娃娃机模拟系统,重点讲解爪子与奖品的碰撞检测、最优抓取姿态计算,以及商家常用的概率控制策略。
核心算法一:几何抓取规划
问题建模
将三维娃娃机简化为二维俯视图(或侧视图截面),系统包含以下几何对象:
– 机械爪(Claw):由三个可开合的夹指组成,夹指末端为圆形碰撞体
– 奖品(Prize):简化为圆形或矩形刚体
– 投放口(Drop Zone):矩形目标区域
抓取成功的判定条件:
1. 爪子下降过程中与奖品发生有效碰撞
2. 闭合时夹指包围范围覆盖奖品重心
3. 提升过程中奖品不滑落(基于摩擦与重心稳定性)
夹指-奖品碰撞检测
每个夹指简化为一条带圆帽的线段(capsule)。检测线段与圆形奖品的相交:
import java.util.*;
/**
* 二维向量与几何工具
*/
class Vec2 {
public double x, y;
public Vec2(double x, double y) { this.x = x; this.y = y; }
public Vec2 add(Vec2 v) { return new Vec2(x + v.x, y + v.y); }
public Vec2 sub(Vec2 v) { return new Vec2(x - v.x, y - v.y); }
public Vec2 mul(double s) { return new Vec2(x * s, y * s); }
public double dot(Vec2 v) { return x * v.x + y * v.y; }
public double lenSq() { return x * x + y * y; }
public double len() { return Math.sqrt(lenSq()); }
public Vec2 normalize() {
double l = len();
return l < 1e-9 ? new Vec2(0, 0) : new Vec2(x / l, y / l);
}
public Vec2 rotate(double angle) {
double cos = Math.cos(angle), sin = Math.sin(angle);
return new Vec2(x * cos - y * sin, x * sin + y * cos);
}
}
/**
* 圆形奖品
*/
class Prize {
Vec2 pos; // 重心位置
double radius; // 外接圆半径
double mass; // 质量(影响滑落概率)
double friction;// 表面摩擦系数
boolean grabbed;// 是否被抓住
public Prize(double x, double y, double r, double m, double f) {
this.pos = new Vec2(x, y);
this.radius = r;
this.mass = m;
this.friction = f;
this.grabbed = false;
}
}
/**
* 机械爪夹指(capsule碰撞体)
*/
class ClawFinger {
Vec2 base; // 根部连接点(相对于爪子中心)
Vec2 tip; // 末端位置
double tipRadius = 8; // 末端圆帽半径
double maxLength = 60; // 夹指最大延伸长度
public ClawFinger(Vec2 base) {
this.base = base;
this.tip = base;
}
/**
* 根据开合角度更新末端位置
* @param openAngle 开合角度(0=完全闭合,60度=完全张开)
*/
public void updateTip(double openAngle) {
Vec2 dir = base.normalize().rotate(openAngle * Math.PI / 180);
tip = base.add(dir.mul(maxLength));
}
}
/**
* 几何碰撞检测器
*/
class GeometryUtils {
/**
* 计算点到线段的最短距离平方
* @param p 点
* @param a 线段起点
* @param b 线段终点
*/
public static double pointToSegmentDistSq(Vec2 p, Vec2 a, Vec2 b) {
Vec2 ab = b.sub(a);
Vec2 ap = p.sub(a);
double t = Math.max(0, Math.min(1, ap.dot(ab) / ab.lenSq()));
Vec2 closest = a.add(ab.mul(t));
return p.sub(closest).lenSq();
}
/**
* 检测capsule(线段+圆帽)与圆的碰撞
*/
public static boolean capsuleCircleCollision(ClawFinger finger,
Vec2 clawCenter,
Prize prize) {
Vec2 a = clawCenter.add(finger.base);
Vec2 b = clawCenter.add(finger.tip);
double distSq = pointToSegmentDistSq(prize.pos, a, b);
double totalRadius = finger.tipRadius + prize.radius;
return distSq <= totalRadius * totalRadius;
}
/**
* 计算夹指对奖品的包围度(0~1,越高越稳定)
* 通过计算奖品重心到三夹指形成三角形的重心距离来评估
*/
public static double calcGraspStability(ClawFinger[] fingers,
Vec2 clawCenter,
Prize prize) {
Vec2[] tips = new Vec2[3];
for (int i = 0; i < 3; i++) {
tips[i] = clawCenter.add(fingers[i].tip);
}
// 三夹指末端形成三角形的重心
Vec2 triCenter = tips[0].add(tips[1]).add(tips[2]).mul(1.0 / 3.0);
double dist = prize.pos.sub(triCenter).len();
// 归一化:距离越小包围度越高
return Math.max(0, 1 - dist / 50.0);
}
}
核心算法二:概率控制与期望价值
商家通常不会让娃娃机成为纯粹的技巧游戏,而是通过概率控制调节玩家的胜率。常见策略包括:
- 力度控制:爪子闭合力度按概率分布,有时强有时弱
- 滑落控制:即使抓住,提升阶段也有概率滑落
- 强抓周期:每N次中设置1次”必中”机会(强力爪)
概率模型
定义三阶段成功率:
– $P_{grab}$:闭合阶段成功夹住奖品的概率(与包围度正相关,但受商家设定上限)
– $P_{hold}$:提升阶段保持不掉的概率(与奖品摩擦、质量相关)
– $P_{win}$:最终获胜概率 = $P_{grab} \times P_{hold}$
商家通过调整 $P_{win}$ 的目标期望值来控制盈利。
import java.util.Random;
/**
* 娃娃机概率控制器
*/
class ProbabilityController {
private Random rand = new Random();
private double targetWinRate; // 目标胜率(如0.15表示15%)
private int totalPlays = 0; // 总游戏次数
private int totalWins = 0; // 总获胜次数
private int forcedWinCounter; // 强制必中计数器
private int forcedWinInterval; // 强制必中间隔
public ProbabilityController(double targetRate, int forcedInterval) {
this.targetWinRate = targetRate;
this.forcedWinInterval = forcedInterval;
this.forcedWinCounter = rand.nextInt(forcedInterval);
}
/**
* 判定本次游戏是否为强制必中
*/
public boolean isForcedWin() {
forcedWinCounter++;
if (forcedWinCounter >= forcedWinInterval) {
forcedWinCounter = 0;
return true;
}
return false;
}
/**
* 计算当前闭合力度系数(0~1)
* @param graspStability 几何包围度(0~1)
*/
public double calcGripStrength(double graspStability) {
if (isForcedWin()) {
return 1.0; // 强力爪:最大力度
}
// 动态调整:当前胜率低于目标时适当提高力度
double currentRate = totalPlays > 0 ? (double) totalWins / totalPlays : 0;
double adjustment = Math.max(0, (targetWinRate - currentRate) * 2);
// 基础力度 = 包围度 * (基础系数 + 动态调整)
double baseStrength = graspStability * (0.3 + adjustment);
return Math.min(1.0, baseStrength);
}
/**
* 判定提升阶段是否滑落
* @param prize 奖品属性
* @param gripStrength 当前闭合力度
*/
public boolean doesPrizeSlip(Prize prize, double gripStrength) {
// 滑落概率 = (1 - 力度) * 质量因子 * (1 - 摩擦系数)
double massFactor = Math.min(1.0, prize.mass / 2.0);
double slipProb = (1 - gripStrength) * massFactor * (1 - prize.friction);
return rand.nextDouble() < slipProb;
}
public void recordResult(boolean win) {
totalPlays++;
if (win) totalWins++;
}
public double getActualWinRate() {
return totalPlays > 0 ? (double) totalWins / totalPlays : 0;
}
}
核心算法三:最优抓取姿态搜索
对于自动操控或辅助瞄准系统,需要搜索最佳的爪子下落位置与角度。采用网格采样+评分函数策略:
/**
* 最优抓取姿态搜索器
*/
class GraspPlanner {
private static final int GRID_SIZE = 10; // 10x10网格采样
/**
* 在候选区域内搜索最优下落位置
* @param prizes 所有奖品列表
* @param clawRange 爪子可移动范围(矩形)
* @return 最优爪子中心位置
*/
public Vec2 findBestDropPosition(List<Prize> prizes,
double[] clawRange) {
double minX = clawRange[0], maxX = clawRange[1];
double minY = clawRange[2], maxY = clawRange[3];
double bestScore = -1;
Vec2 bestPos = new Vec2((minX + maxX) / 2, (minY + maxY) / 2);
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
double x = minX + (maxX - minX) * i / (GRID_SIZE - 1);
double y = minY + (maxY - minY) * j / (GRID_SIZE - 1);
Vec2 pos = new Vec2(x, y);
double score = evaluatePosition(pos, prizes);
if (score > bestScore) {
bestScore = score;
bestPos = pos;
}
}
}
return bestPos;
}
/**
* 评分函数:综合考虑可达奖品数量、价值、抓取稳定性
*/
private double evaluatePosition(Vec2 pos, List<Prize> prizes) {
double score = 0;
ClawFinger[] fingers = createFingers(30); // 30度张开
for (Prize p : prizes) {
// 距离因子:越近越好
double dist = p.pos.sub(pos).len();
if (dist > 100) continue; // 太远不可达
double distScore = 1 - dist / 100.0;
// 计算该位置对此奖品的包围度
double stability = GeometryUtils.calcGraspStability(fingers, pos, p);
// 价值加权(假设质量越大的奖品越有价值)
double value = p.mass;
score += distScore * stability * value;
}
return score;
}
private ClawFinger[] createFingers(double openAngle) {
ClawFinger[] fingers = new ClawFinger[3];
for (int i = 0; i < 3; i++) {
double baseAngle = i * 2 * Math.PI / 3;
fingers[i] = new ClawFinger(
new Vec2(Math.cos(baseAngle) * 10, Math.sin(baseAngle) * 10)
);
fingers[i].updateTip(openAngle);
}
return fingers;
}
}
完整游戏流程控制
/**
* 娃娃机游戏主控
*/
public class ClawMachineGame {
private List<Prize> prizes = new ArrayList<>();
private Vec2 clawPos = new Vec2(0, -150);
private double clawZ = 0; // 高度:0=顶部,-100=底部
private ClawFinger[] fingers = new ClawFinger[3];
private ProbabilityController probCtrl;
private GraspPlanner planner;
private GameState state = GameState.IDLE;
private Prize grabbedPrize = null;
enum GameState { IDLE, DROPPING, GRABBING, LIFTING, MOVING_TO_DROP, RELEASING }
public ClawMachineGame() {
probCtrl = new ProbabilityController(0.20, 8); // 20%目标胜率,每8次强制1次
planner = new GraspPlanner();
initFingers();
spawnPrizes();
}
private void initFingers() {
for (int i = 0; i < 3; i++) {
double angle = i * 2 * Math.PI / 3;
fingers[i] = new ClawFinger(
new Vec2(Math.cos(angle) * 10, Math.sin(angle) * 10)
);
}
}
private void spawnPrizes() {
// 随机生成奖品
for (int i = 0; i < 8; i++) {
prizes.add(new Prize(
(Math.random() - 0.5) * 200,
(Math.random() - 0.5) * 100 + 50,
15 + Math.random() * 15,
0.5 + Math.random() * 1.5,
0.3 + Math.random() * 0.4
));
}
}
/**
* 执行一次完整的自动抓取流程
*/
public boolean playAutoRound() {
// 1. 规划最优位置
Vec2 bestPos = planner.findBestDropPosition(prizes,
new double[]{-150, 150, -100, 100});
clawPos = bestPos;
// 2. 下降阶段
state = GameState.DROPPING;
clawZ = -100;
// 3. 检测碰撞,选择目标奖品
Prize target = findBestTarget();
if (target == null) {
state = GameState.IDLE;
probCtrl.recordResult(false);
return false;
}
// 4. 闭合阶段
state = GameState.GRABBING;
double stability = GeometryUtils.calcGraspStability(fingers, clawPos, target);
double gripStrength = probCtrl.calcGripStrength(stability);
// 5. 提升阶段:判定是否滑落
state = GameState.LIFTING;
clawZ = 0;
boolean slip = probCtrl.doesPrizeSlip(target, gripStrength);
if (!slip && gripStrength > 0.5) {
// 成功抓取
grabbedPrize = target;
prizes.remove(target);
state = GameState.MOVING_TO_DROP;
state = GameState.RELEASING;
probCtrl.recordResult(true);
return true;
} else {
// 抓取失败
state = GameState.IDLE;
probCtrl.recordResult(false);
return false;
}
}
private Prize findBestTarget() {
Prize best = null;
double bestScore = -1;
for (Prize p : prizes) {
double dist = p.pos.sub(clawPos).len();
if (dist > 80) continue;
double score = GeometryUtils.calcGraspStability(fingers, clawPos, p);
if (score > bestScore) {
bestScore = score;
best = p;
}
}
return best;
}
}
算法复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 碰撞检测 | $O(n)$ | $O(1)$ | n为奖品数量,每帧检测所有候选 |
| 包围度计算 | $O(1)$ | $O(1)$ | 固定3个夹指,常数时间 |
| 概率判定 | $O(1)$ | $O(1)$ | 随机数生成与比较 |
| 姿态搜索 | $O(k^2 \cdot n)$ | $O(1)$ | k×k网格,每个位置评估n个奖品 |
扩展思路
- 三维扩展:引入3D碰撞检测(球体与圆柱体/胶囊体),爪子角度增加俯仰与偏航维度
- 机器学习辅助:用神经网络预测不同奖品的最优抓取姿态,通过大量仿真数据训练
- 玩家心理模型:根据玩家历史行为动态调整概率曲线,维持”差一点就抓到”的体验
总结
本文实现了一个娃娃机模拟系统,核心算法涵盖:
– Capsule-圆形碰撞检测,将夹指建模为线段+圆帽进行高效相交判定
– 概率控制模型,通过目标胜率、动态调整和强制必中周期来平衡游戏性与商业收益
– 网格采样搜索,在离散候选位置中寻找最优下落点
这些算法不仅适用于娃娃机,其碰撞检测框架可复用于任意胶囊体与圆的相交场景,概率控制思想亦可迁移至抽奖、抽卡等系统的掉落率设计中。