15、游戏开发:用加速度传感器控制小球移动

说实话,加速度传感器最酷的应用场景,就是做游戏控制。

你想想看,手机就是游戏手柄,倾斜手机就能控制角色移动——这种体验比点按屏幕爽多了。我最早接触这个玩法,是在一个打砖块游戏里,用倾斜控制挡板接球。当时我就想,这玩意儿要是用在自家项目里该多好。

今天我们就来实现一个经典案例:用加速度传感器控制一个小球在屏幕上移动。小球会随着手机倾斜的方向滚动,倾斜角度越大,滚得越快。

核心思路

说白了,就是把传感器的数值映射到小球的位置变化上。

  • X轴加速度 → 控制小球左右移动
  • Y轴加速度 → 控制小球上下移动
  • Z轴 → 暂时忽略,或者用来做其他效果

但这里有个坑:传感器返回的是加速度值,不是位移值。你不能直接把加速度当位置用,否则小球会飞出去。正确的做法是:用加速度值作为速度的增量,然后速度再影响位置。

核心公式:

速度 += 加速度 × 时间步长

位置 += 速度 × 时间步长

再加上阻尼系数,让小球停下来。

代码实现

我们先写一个简单的游戏循环。我习惯用 SurfaceView 来做实时渲染,因为它可以在子线程里画图,不会卡住主线程。

public class BallGameView extends SurfaceView implements SurfaceHolder.Callback, SensorEventListener {

    private Paint ballPaint;
    private float ballX, ballY;
    private float ballRadius = 40f;
    private float vx = 0f, vy = 0f;
    private float damping = 0.95f; // 阻尼系数
    private float scaleFactor = 2.0f; // 加速度映射到速度的系数

    private SensorManager sensorManager;
    private Sensor accelerometer;

    public BallGameView(Context context) {
        super(context);
        getHolder().addCallback(this);
        ballPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        ballPaint.setColor(0xFF4CAF50);

        sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        float ax = event.values[0]; // X轴加速度
        float ay = event.values[1]; // Y轴加速度

        // 注意:手机平放时,X和Y接近0,Z接近9.8
        // 我们只关心水平方向的倾斜
        vx += ax * scaleFactor;
        vy += ay * scaleFactor;

        // 应用阻尼,让小球慢慢停下来
        vx *= damping;
        vy *= damping;
    }

    // 在游戏循环中更新位置
    public void update() {
        ballX += vx;
        ballY += vy;

        // 边界碰撞检测
        if (ballX - ballRadius < 0) {
            ballX = ballRadius;
            vx = -vx * 0.5f;
        } else if (ballX + ballRadius > getWidth()) {
            ballX = getWidth() - ballRadius;
            vx = -vx * 0.5f;
        }

        if (ballY - ballRadius < 0) {
            ballY = ballRadius;
            vy = -vy * 0.5f;
        } else if (ballY + ballRadius > getHeight()) {
            ballY = getHeight() - ballRadius;
            vy = -vy * 0.5f;
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        canvas.drawColor(0xFFF5F5F5);
        canvas.drawCircle(ballX, ballY, ballRadius, ballPaint);
    }

    // 省略 SurfaceHolder.Callback 和 SensorEventListener 的其他方法
}

小技巧:阻尼系数 damping 我一般设在 0.9~0.98 之间。数值越大,小球滑行越久,手感越「飘」。数值越小,小球停得越快,手感越「硬」。你可以根据游戏类型调整。

坐标系问题

这里有个容易搞混的地方:传感器的坐标系和屏幕坐标系是反的

手机平放时,传感器 X 轴向右为正,Y 轴向上为正。但屏幕的 Y 轴是向下为正。所以如果你直接把 Y 轴加速度传给小球,小球会往反方向跑。

解决办法很简单:把 Y 轴加速度取反。

vy += -ay * scaleFactor; // 注意负号

嗯,这个坑我踩过。第一次写的时候,手机往左倾斜,小球往右跑,我还以为是传感器坏了……

知识体系图

下面这张图帮你理清整个控制链路:

加速度传感器控制小球移动 — 数据流 加速度传感器 X, Y, Z 原始数据 数据处理 取反 + 缩放 + 阻尼 物理引擎 速度更新 + 位置更新 碰撞检测 边界反弹 + 能量损失 渲染绘制 Canvas.drawCircle() 游戏循环(每帧重复)

调参经验

写代码容易,调参数才是真功夫。我分享几个经验值:

参数 推荐范围 说明
scaleFactor 1.0 ~ 5.0 控制灵敏度。数值越大,小球反应越灵敏
damping 0.90 ~ 0.98 控制滑行感。数值越大越滑,越小越硬
碰撞恢复系数 0.3 ~ 0.7 控制反弹力度。0.5 左右手感比较自然
更新频率 30 ~ 60 FPS 游戏循环的帧率,建议用固定时间步长

注意:千万不要在 onSensorChanged() 里直接更新 UI 或 Canvas。这个回调频率很高(可能几百赫兹),直接画图会导致界面卡死。正确的做法是:在回调里只更新数据,然后在游戏循环的 update()draw() 里使用这些数据。

扩展思路

基础功能做完了,你可以加点花样:

  • 多球模式:创建多个小球,各自独立运动
  • 重力场效果:用 Z 轴加速度改变重力方向
  • 迷宫游戏:把小球限制在迷宫通道里,倾斜手机引导小球走出迷宫
  • 粒子系统:小球撞墙时产生粒子特效

我个人觉得,迷宫游戏是最适合练手的。你只需要在 Canvas 上画一些障碍物,然后检测小球和障碍物的碰撞即可。我曾经用这个思路做过一个「滚珠迷宫」的小游戏,用户反馈特别好。

好了,今天的核心内容就这些。记住:传感器控制游戏的关键在于「加速度→速度→位置」的映射,再加上阻尼和碰撞,手感就能调出来。多试几次参数,找到最适合你游戏的感觉。