在这里插入图片描述
个人主页:ujainu

前言

在移动开发领域,尤其是跨平台框架如 Flutter 与新兴操作系统 OpenHarmony 的融合生态中,开发者常常需要在不依赖复杂物理引擎(如 Box2D、Flame Physics)的前提下,实现具有“吸附轨道”效果的交互逻辑。这类需求常见于太空探索类小游戏、粒子动画、UI 动效等场景。

本文将深入剖析如何仅通过基础数学运算(向量距离、圆形碰撞检测、切向速度计算、点积判断入射方向)来构建一个完整的“吸附轨道”系统,并采用 状态机(State Machine) 模式管理对象的 orbiting(绕轨)与 flying(飞行)两种状态。我们将全程使用 Dart 语言,在 Flutter + OpenHarmony 环境下实现,代码可直接运行,且性能高效、逻辑清晰。


一、核心思想:用数学代替物理引擎

传统物理引擎虽强大,但引入后会显著增加包体积与运行开销。对于轻量级交互(如 UI 动画、简单游戏),我们完全可以利用以下数学工具替代:

  • 向量距离:判断物体是否进入引力范围
  • 圆形碰撞检测:简化为点到圆心的距离比较
  • atan2(dy, dx):计算目标角度
  • cos/sin:将极坐标转为直角坐标
  • 点积(dot product):判断物体是“靠近”还是“远离”中心

这些操作均基于 CPU 计算,无需 GPU 或专用物理库,非常适合 OpenHarmony 这类对资源敏感的嵌入式或轻量化系统。


二、状态机设计:Flying vs Orbiting

我们定义两种状态:

状态 行为描述
Flying 物体自由飞行,受初始速度影响,若进入引力半径则尝试吸附
Orbiting 物体沿圆形轨道绕中心点旋转,保持恒定半径

状态切换条件:

  • Flying → Orbiting:当物体进入引力半径(distance < orbitRadius)且速度方向指向中心(点积 > 0)
  • Orbiting → Flying:用户点击/触发脱离,或速度过大突破轨道

💡 注意:我们不模拟真实引力(如 F = GmM/r²),而是采用“硬吸附”逻辑——一旦满足条件,立即锁定到轨道上,提升响应性与可控性。


三、关键数学公式详解

1. 向量距离与圆形碰撞检测

给定中心点 (cx, cy) 与物体位置 (x, y),距离为:

double dx = x - cx;
double dy = y - cy;
double distance = sqrt(dx * dx + dy * dy);

distance <= orbitRadius,则视为“进入轨道范围”。

2. 使用 atan2 获取当前角度

double angle = atan2(dy, dx); // 返回 [-π, π]

此角度用于后续轨道位置更新。

3. 切向速度计算

绕轨运动需沿切线方向施加速度。切向单位向量为:

// 法向量 (dx, dy) 归一化
double nx = dx / distance;
double ny = dy / distance;

// 切向量:逆时针旋转90度 → (-ny, nx)
double tangentX = -ny;
double tangentY = nx;

若希望顺时针旋转,则取 (ny, -nx)

4. 点积判断入射方向

物体速度向量 (vx, vy) 与指向中心的向量 (-dx, -dy) 的点积:

double dot = vx * (-dx) + vy * (-dy);
  • dot > 0:速度有朝向中心的分量 → 可能被吸附
  • dot <= 0:正在远离 → 不应吸附

✅ 此判断避免了“擦边飞过却被强行吸附”的不合理行为。


四、代码结构设计

我们将创建一个 OrbitObject 类,封装状态、位置、速度、中心点等属性,并提供 update() 方法每帧调用。

enum OrbitState { flying, orbiting }

class OrbitObject {
  Offset position;
  double vx, vy; // 速度
  final Offset center;
  final double orbitRadius;
  final double orbitSpeed; // 弧度/秒

  OrbitState state = OrbitState.flying;
  double currentAngle = 0.0;
  double actualRadius = 0.0;

  OrbitObject({
    required this.position,
    required this.center,
    required this.orbitRadius,
    this.vx = 0,
    this.vy = 0,
    this.orbitSpeed = 2.0, // 默认 2 rad/s
  });
}

五、核心逻辑:update() 方法实现

void update(double deltaTime) {
  final dx = position.dx - center.dx;
  final dy = position.dy - center.dy;
  final distance = sqrt(dx * dx + dy * dy);

  if (state == OrbitState.flying) {
    // 自由飞行:更新位置
    position = Offset(
      position.dx + vx * deltaTime,
      position.dy + vy * deltaTime,
    );

    // 检查是否可吸附
    if (distance <= orbitRadius) {
      // 计算速度与指向中心向量的点积
      final dot = vx * (-dx) + vy * (-dy);
      if (dot > 0) {
        // 切换到 orbiting 状态
        state = OrbitState.orbiting;
        currentAngle = atan2(dy, dx);
        actualRadius = distance; // 锁定当前半径
        // 可选:重置速度为纯切向
        final nx = dx / distance;
        final ny = dy / distance;
        final tangentX = -ny;
        final tangentY = nx;
        // 保留原有切向速度大小,或设为固定值
        final speed = sqrt(vx * vx + vy * vy);
        vx = tangentX * speed;
        vy = tangentY * speed;
      }
    }
  } else if (state == OrbitState.orbiting) {
    // 绕轨运动:更新角度
    currentAngle += orbitSpeed * deltaTime;

    // 保持半径恒定(可微调 actualRadius 实现螺旋)
    position = Offset(
      center.dx + actualRadius * cos(currentAngle),
      center.dy + actualRadius * sin(currentAngle),
    );

    // 可选:计算当前速度用于渲染轨迹
    vx = -actualRadius * orbitSpeed * sin(currentAngle);
    vy = actualRadius * orbitSpeed * cos(currentAngle);
  }
}

🔍 优化提示

  • 避免每帧 sqrt:可用 distanceSquared 比较(dx*dx + dy*dy < orbitRadius*orbitRadius
  • atan2 仅在状态切换时调用一次,后续用角度累加,避免三角函数重复计算

六、Flutter 渲染层集成

在 Flutter 中,我们使用 CustomPainter 绘制轨道与物体,并通过 AnimationController 驱动帧更新。

class OrbitPainter extends CustomPainter {
  final OrbitObject obj;
  final double orbitRadius;

  OrbitPainter(this.obj, this.orbitRadius);

  
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = Colors.blue.withOpacity(0.3);
    // 绘制轨道
    canvas.drawCircle(
      obj.center,
      orbitRadius,
      paint..style = PaintingStyle.stroke..strokeWidth = 2,
    );

    // 绘制物体
    canvas.drawCircle(
      obj.position,
      8,
      paint..color = obj.state == OrbitState.orbiting ? Colors.green : Colors.red,
    );
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

主 Widget 使用 SingleTickerProviderStateMixin 控制动画:

class OrbitDemo extends StatefulWidget {
  
  _OrbitDemoState createState() => _OrbitDemoState();
}

class _OrbitDemoState extends State<OrbitDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late OrbitObject _obj;

  
  void initState() {
    super.initState();
    _obj = OrbitObject(
      position: const Offset(200, 100),
      center: const Offset(200, 300),
      orbitRadius: 100,
      vx: 100, // 初始向右飞行
      vy: 50,
    );
    _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 16), // ~60fps
    )..repeat();
  }

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (details) {
        // 点击脱离轨道
        if (_obj.state == OrbitState.orbiting) {
          _obj.state = OrbitState.flying;
        }
      },
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          _obj.update(1 / 60); // 假设 60fps
          return CustomPaint(
            painter: OrbitPainter(_obj, 100),
            size: Size.infinite,
          );
        },
      ),
    );
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

七、完整可运行代码(贴合主题)

以下代码可在已搭建好的 Flutter + OpenHarmony 环境中直接运行,展示吸附轨道效果:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '吸附轨道 - Flutter + OpenHarmony',
      home: Scaffold(
        body: OrbitDemo(),
      ),
    );
  }
}

enum OrbitState { flying, orbiting }

class OrbitObject {
  Offset position;
  double vx, vy;
  final Offset center;
  final double orbitRadius;
  final double orbitSpeed;

  OrbitState state = OrbitState.flying;
  double currentAngle = 0.0;
  double actualRadius = 0.0;

  OrbitObject({
    required this.position,
    required this.center,
    required this.orbitRadius,
    this.vx = 0,
    this.vy = 0,
    this.orbitSpeed = 2.0,
  });

  void update(double deltaTime) {
    final dx = position.dx - center.dx;
    final dy = position.dy - center.dy;
    final distanceSquared = dx * dx + dy * dy;
    final distance = sqrt(distanceSquared);

    if (state == OrbitState.flying) {
      position = Offset(
        position.dx + vx * deltaTime,
        position.dy + vy * deltaTime,
      );

      if (distanceSquared <= orbitRadius * orbitRadius) {
        final dot = vx * (-dx) + vy * (-dy);
        if (dot > 0) {
          state = OrbitState.orbiting;
          currentAngle = atan2(dy, dx);
          actualRadius = distance;
          final nx = dx / distance;
          final ny = dy / distance;
          final tangentX = -ny;
          final tangentY = nx;
          final speed = sqrt(vx * vx + vy * vy);
          vx = tangentX * speed;
          vy = tangentY * speed;
        }
      }
    } else {
      currentAngle += orbitSpeed * deltaTime;
      position = Offset(
        center.dx + actualRadius * cos(currentAngle),
        center.dy + actualRadius * sin(currentAngle),
      );
      vx = -actualRadius * orbitSpeed * sin(currentAngle);
      vy = actualRadius * orbitSpeed * cos(currentAngle);
    }
  }
}

class OrbitPainter extends CustomPainter {
  final OrbitObject obj;
  final double orbitRadius;

  OrbitPainter(this.obj, this.orbitRadius);

  
  void paint(Canvas canvas, Size size) {
    final center = obj.center;
    final paint = Paint();

    // 轨道
    canvas.drawCircle(
      center,
      orbitRadius,
      paint
        ..color = Colors.blue.withOpacity(0.2)
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2,
    );

    // 物体
    canvas.drawCircle(
      obj.position,
      10,
      paint..color = obj.state == OrbitState.orbiting ? Colors.green : Colors.red,
    );

    // 中心点
    canvas.drawCircle(center, 5, paint..color = Colors.black);
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

class OrbitDemo extends StatefulWidget {
  
  _OrbitDemoState createState() => _OrbitDemoState();
}

class _OrbitDemoState extends State<OrbitDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late OrbitObject _obj;

  
  void initState() {
    super.initState();
    _obj = OrbitObject(
      position: const Offset(150, 100),
      center: const Offset(200, 300),
      orbitRadius: 120,
      vx: 120,
      vy: 80,
    );
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 16),
    )..repeat();
  }

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        if (_obj.state == OrbitState.orbiting) {
          _obj.state = OrbitState.flying;
        }
      },
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          _obj.update(1 / 60);
          return CustomPaint(
            painter: OrbitPainter(_obj, 120),
            size: MediaQuery.of(context).size,
          );
        },
      ),
    );
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

运行界面:
在这里插入图片描述


八、总结与扩展

本文通过纯数学方法实现了无物理引擎的吸附轨道系统,具备以下优势:

  • 轻量高效:无外部依赖,适合 OpenHarmony 资源受限设备
  • 逻辑可控:状态机清晰,易于调试与扩展(如添加多轨道、螺旋吸入)
  • 跨平台兼容:Dart 代码在 Flutter 支持的所有平台(包括 OpenHarmony)均可运行

可扩展方向

  • 添加多个引力中心,实现复杂轨道
  • 引入阻尼系数,模拟能量损耗
  • 结合手势拖拽,实现“投掷吸附”交互

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐