当前位置: 首页 > article >正文

【Unity】重力场中的路径预测方法

前言

笔者前些天参加完了一场72小时的GameJam游戏开发比赛。这次比赛的主题是“探索”,笔者做了一个名为《探索者号》的探索宇宙的游戏(游戏名一开始叫做《星际拾荒者》,但这不重要)。

在开发过程中,笔者遇到了一些问题,特此做下记录和分享,希望对大家和今后的我有所帮助。

笔者本次的参赛作品,在实现路径预测可视化时使用了RK4方法,效果还不错:

【72小时极限游戏开发挑战赛】探索者号

《探索者号》核心玩法

  • 玩家可以控制飞船加速和转向,并可以射击障碍物来保证自身不被撞毁,探索7颗星球。
  • 玩家的每个操作,还有随着时间流逝,都会消耗燃料。
  • 燃料耗尽后,玩家将无法操控飞船,但5秒后会消耗生命值来补充一定燃料。
  • 玩家每接近一个星球,并使用引力弹弓离开时,会将燃料加满。
  • 生命值耗尽,游戏失败。

简而言之,就是借助引力弹弓来补充燃料和加速,以到达更远距离,探索更多星球的目的。

重力场中路径预测可视化

为什么有这种需求?

该游戏的难点在于,玩家无法凭空推算出或感觉出在飞船靠近行星时,应该如何调整自身方向,才能保证不撞到星球上,并且完成有效的“引力弹弓”动作。
所以笔者希望在游戏中添加一条路径预测的引导线,有了这根线,将大大降低新人玩家的上手难度。

核心思路

笔者的方法是在飞船对象上附加一个LineRenderer组件,利用它来绘制飞船在未来时间点的预测路径。

具体实现方式:首先利用当前的飞船速度和所受的引力影响,计算出飞船在“下一瞬间”的预期位置,并将这个位置设置为LineRenderer的第一个节点。接着基于这个预测位置,再计算出飞船在“下下一瞬间”的位置,将其设置为LineRenderer的第二个节点。通过重复这一过程,我们能够逐步构建出一系列时间点上的飞船位置节点。
通过将这些节点相连,形成了一条连续的引导线,这条引导线基于飞船的初始速度向量、飞船与行星之间的引力互动、以及它们的相对位置关系。这样在单个渲染帧内,我们就能够预测并展示飞船在接下来一段时间内的运动轨迹。

这种可视化的路径预测不仅增强了游戏的互动性和玩家的体验,还提供了一个直观的方式来理解和预测物体在复杂重力场中的动态行为。通过这种方法,玩家可以更好地规划飞船的航线,避免撞到星球,优化飞行轨迹。

常规方法

高中物理,略。

计算过程:
对于每个时间点  i :  预测位置:  S i = S i − 1 + U i t + 1 2 a i t 2  更新加速度:  a i = gravityStrength r i 2  更新速度:  V i = U i − 1 + a i t (假设 G × M 为行星的重力强度: g r a v i t y S t r e n g t h ) \text{对于每个时间点 } i: \\ \text{ 预测位置: } S_i = S_{i-1} + U_it + \frac{1}{2}a_it^2 \\ \text{ 更新加速度: } a_i = \frac{\text{gravityStrength}}{r_i^2} \\ \text{ 更新速度: } V_i = U_{i-1} + a_it \\ \\ (假设G×M为行星的重力强度:gravityStrength) 对于每个时间点 i 预测位置: Si=Si1+Uit+21ait2 更新加速度: ai=ri2gravityStrength 更新速度: Vi=Ui1+ait(假设G×M为行星的重力强度:gravityStrength

核心代码:

    // 目标行星Transform
    public Transform targetPlanet;
    // 行星重力强度
    public float gravityStrength;
    // 路径点数
    public int pathResolution = 50;
    // 预测路径总时长
    public float pathPredictTime = 5f;

    private LineRenderer lineRenderer;

    void Start()
    {
        lineRenderer = gameObject.AddComponent<LineRenderer>();
        lineRenderer.positionCount = pathResolution;
    }

    void Update()
    {
        // 其他运动逻辑

        // 调用UpdatePath进行路径预测
        UpdatePath(currentPos, currentVelocity, currentAcceleration);
    }

    // 更新路径预测
    private void UpdatePath(Vector2 currentPos, Vector2 currentVelocity, Vector2 currentAcceleration)
    {
        // 每一步的时间间隔
        float t = pathPredictTime / pathResolution;

        for (int i = 0; i < pathResolution; i++)
        {
            // 使用基本运动方程预测位置
            Vector2 predictedPos = currentPos + currentVelocity * t + 0.5f * currentAcceleration * t * t;

            // 将计算的位置设置为轨迹的一部分
            lineRenderer.SetPosition(i, predictedPos);

            // 基于新的预测位置,计算下一点的重力加速度
            Vector2 gravityDirection = (Vector2)targetPlanet.position - predictedPos;
            currentAcceleration = gravityDirection.normalized * gravityStrength / gravityDirection.sqrMagnitude;

            // 更新当前位置和速度
            currentPos = predictedPos;
            currentVelocity += currentAcceleration * t;
        }
    }

RK4方法

Runge-Kutta第四阶(RK4)算法,是一种用于求解常微分方程初值问题的数值方法。给定一个常微分方程
d y d t = f ( t , y ) \frac{\mathrm{d} y}{\mathrm{d} t} = f(t,y) dtdy=f(t,y),
及其初始条件
y ( t 0 ) = y 0 y(t_0)=y_0 y(t0)=y0,
RK4方法通过以下步骤来估计在处的值,其中 h h h是步长:
k 1 = f ( t , y ) , k 2 = f ( t + h 2 , y + h 2 k 1 ) , k 3 = f ( t + h 2 , y + h 2 k 2 ) , k 4 = f ( t + h , y + h k 3 ) , y ( t + h ) = y + h 6 ( k 1 + 2 k 2 + 2 k 3 + k 4 ) . \begin{align*} k_1 &= f(t, y), \\ k_2 &= f\left(t + \frac{h}{2}, y + \frac{h}{2}k_1\right), \\ k_3 &= f\left(t + \frac{h}{2}, y + \frac{h}{2}k_2\right), \\ k_4 &= f(t + h, y + hk_3), \\ \\ y(t + h) &= y + \frac{h}{6}(k_1 + 2k_2 + 2k_3 + k_4). \end{align*} k1k2k3k4y(t+h)=f(t,y),=f(t+2h,y+2hk1),=f(t+2h,y+2hk2),=f(t+h,y+hk3),=y+6h(k1+2k2+2k3+k4).

这个过程提供了一种高精度的方式来逼近常微分方程的解,通过将整个步长 h h h分为更小的部分并计算在这些部分上的斜率,然后将这些斜率的加权平均值用于最终的估计。

应用到游戏中:
△ t = T n k 1 v = v k 1 a = a ( p ) k 2 v = v + k 1 a ⋅ Δ t 2 k 2 a = a ( p + k 1 v ⋅ Δ t 2 ) k 3 v = v + k 2 a ⋅ Δ t 2 k 3 a = a ( p + k 2 v ⋅ Δ t 2 ) k 4 v = v + k 3 a ⋅ Δ t k 4 a = a ( p + k 3 v ⋅ Δ t ) v new = v + ( k 1 a + 2 k 2 a + 2 k 3 a + k 4 a ) ⋅ Δ t 6 p new = p + ( k 1 v + 2 k 2 v + 2 k 3 v + k 4 v ) ⋅ Δ t 6 a ( p ) = g ⋅ r 2 ∥ d ∥ 2 其中: 初始位置 p 和速度 v 需要根据游戏中实际情况确定 △ t :每一步的时间间隔 T :总预测时间 n :分辨率(对应 L i n e R e n d e r e r 的节点数) k 1... k 4 :四组斜率 d :物体到行星中心的向量 g :模拟行星重力强度(相当于 G M ) r :行星半径 ∥ d ∥ 2 : d 的平方模长 a :加速度 \begin{align*} \triangle t &= \frac{T}{n} \\ k1_v &= v \\ k1_a &= a(p) \\ k2_v &= v + k1_a \cdot \frac{\Delta t}{2} \\ k2_a &= a\left(p + k1_v \cdot \frac{\Delta t}{2}\right) \\ k3_v &= v + k2_a \cdot \frac{\Delta t}{2} \\ k3_a &= a\left(p + k2_v \cdot \frac{\Delta t}{2}\right) \\ k4_v &= v + k3_a \cdot \Delta t \\ k4_a &= a(p + k3_v \cdot \Delta t) \\ \\ v_{\text{new}} &= v + \frac{(k1_a + 2k2_a + 2k3_a + k4_a) \cdot \Delta t}{6} \\ p_{\text{new}} &= p + \frac{(k1_v + 2k2_v + 2k3_v + k4_v) \cdot \Delta t}{6} \\ \\ a(p) &= \frac{g \cdot r^2}{\|d\|^2} \\ 其中 :& \\ &初始位置p和速度v需要根据游戏中实际情况确定 \\ \triangle t&:每一步的时间间隔 \\ T &:总预测时间 \\ n &:分辨率(对应LineRenderer的节点数) \\ k1...k4 &:四组斜率 \\ d &:物体到行星中心的向量 \\ g &:模拟行星重力强度(相当于GM) \\ r &:行星半径 \\ \|d\|^2 &:d的平方模长 \\ a &:加速度 \end{align*} tk1vk1ak2vk2ak3vk3ak4vk4avnewpnewa(p)其中:tTnk1...k4dgrd2a=nT=v=a(p)=v+k1a2Δt=a(p+k1v2Δt)=v+k2a2Δt=a(p+k2v2Δt)=v+k3aΔt=a(p+k3vΔt)=v+6(k1a+2k2a+2k3a+k4a)Δt=p+6(k1v+2k2v+2k3v+k4v)Δt=d2gr2初始位置p和速度v需要根据游戏中实际情况确定:每一步的时间间隔:总预测时间:分辨率(对应LineRenderer的节点数):四组斜率:物体到行星中心的向量:模拟行星重力强度(相当于GM:行星半径d的平方模长:加速度

核心代码:

using UnityEngine;

public class PathPrediction : MonoBehaviour
{
    // 玩家的初始位置和速度
    public Vector2 initialPosition;
    public Vector2 initialVelocity;

    // 表示重力场源的行星
    public Transform planetTransform;
    // 行星的重力强度
    public float planetGravity;
    // 行星的半径
    public float planetRadius;

    // 路径分辨率,即路径上的点数
    public int pathResolution = 100;
    // 预测路径的总时长
    public float pathPredictTime = 5f;

    private LineRenderer lineRenderer;

    private void Start()
    {
        lineRenderer = GetComponent<LineRenderer>();
        lineRenderer.positionCount = pathResolution;

        UpdatePathWithRK4();
    }

    // 使用RK4算法更新路径
    private void UpdatePathWithRK4()
    {
        Vector2 currentPos = initialPosition;
        Vector2 currentVelocity = initialVelocity;
        float deltaTime = pathPredictTime / pathResolution;

        for (int i = 0; i < pathResolution; i++)
        {
            // RK4方法的四个步骤
            Vector2 k1_vel = currentVelocity;
            Vector2 k1_acc = CalculateAcceleration(currentPos);

            Vector2 k2_vel = currentVelocity + k1_acc * (deltaTime / 2f);
            Vector2 k2_acc = CalculateAcceleration(currentPos + k1_vel * (deltaTime / 2f));

            Vector2 k3_vel = currentVelocity + k2_acc * (deltaTime / 2f);
            Vector2 k3_acc = CalculateAcceleration(currentPos + k2_vel * (deltaTime / 2f));

            Vector2 k4_vel = currentVelocity + k3_acc * deltaTime;
            Vector2 k4_acc = CalculateAcceleration(currentPos + k3_vel * deltaTime);

            // 使用四个斜率的加权平均值来更新速度和位置
            currentVelocity += (k1_acc + 2f * (k2_acc + k3_acc) + k4_acc) * (deltaTime / 6f);
            currentPos += (k1_vel + 2f * (k2_vel + k3_vel) + k4_vel) * (deltaTime / 6f);

            // 更新LineRenderer以显示路径
            lineRenderer.SetPosition(i, new Vector3(currentPos.x, currentPos.y, 0));
        }
    }

    // 计算给定位置处的加速度,考虑重力场的影响
    private Vector2 CalculateAcceleration(Vector2 position)
    {
        Vector2 gravityDirection = (Vector2)planetTransform.position - position;
        // 使用万有引力公式计算加速度
        return gravityDirection.normalized * (planetGravity * Mathf.Pow(planetRadius, 2) / gravityDirection.sqrMagnitude);
    }
}

总结

简单方法

优点:

  • 简单直观,适用于线性系统或短时间内预测。
  • 计算速度快。

缺点:

  • 对于非线性系统或需要长时间预测的情况,简单的逼近方法可能不够精确,尤其是在引力场强烈变化的区域。

RK4方法

优点:

  • 精度高,适用于复杂的动态系统,特别是需要准确模拟物理行为的系统。
  • 稳定性强,在处理较平滑的动力学问题,拥有较高的稳定性。

缺点:

  • 与简单方法相比,RK4需要在每个时间步长中计算四次斜率,这增加了每个时间步的计算负担。
  • 实现更复杂,不易理解,需要更多的编码工作和调试。

(本游戏由于场景简单,多在路径预测上多花些资源也不算过分,于是使用了RK4方法,效果如文章开头的视频中所示)
请添加图片描述

实际使用中,我们可以根据不同的场景,选择更加合适的方法。

大佬们如果有优化思路,或者更多实现方式,也请多多指点!

吉祥话

最后祝大家新年快乐,长命百岁!


http://www.kler.cn/a/232738.html

相关文章:

  • 链游系统定制化开发:引领游戏产业的新时代
  • 机器学习——损失函数、代价函数、KL散度
  • Linux相关习题-gcc-gdb-冯诺依曼
  • Java 堆内存管理详解:`-Xms` 和 `-Xmx` 参数的使用与默认内存设置
  • LLMs之Code:Github Spark的简介、安装和使用方法、案例应用之详细攻略
  • uniapp 设置安全区域
  • 排序算法---插入排序
  • 在django中集成markdown文本框
  • Unity类银河恶魔城学习记录5-1.5-2 P62-63 Creating Player Manager and Skill Manager源代码
  • golang 通过 cgo 调用 C++ 库
  • 2024.1.30力扣每日一题——使循环数组所有元素相等的最少秒数
  • 【MySQL进阶之路】SpringBoot 底层如何去和 MySQL 交互了呢?
  • 浏览器提示ERR_SSL_KEY_USAGE_INCOMPATIBLE解决
  • Node.js JSON Schema Ajv依赖库逐步介绍验证类型和中文错误提示
  • elementui上传文件不允许重名
  • Java中 使用Lambda表达式实现模式匹配和类型检查
  • 云服务器也能挂游戏 安卓模拟器
  • 树莓派-Ubuntu22.04
  • 【Unity游戏设计】跳一跳Day1
  • 深度学习预备知识1——数据操作
  • 设置了.gitignore文件,但某些需要被忽略的文件仍然显示
  • Git介绍和常用命令说明
  • 微软.NET6开发的C#特性——委托和事件
  • SpringMVC-组件解析
  • vscode 括号 python函数括号补全
  • 【Flink】FlinkSQL的DataGen连接器(测试利器)