【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.mfbz.cn/a/379734.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Django的配置文件setting.py

BASE_DIR 项目路径&#xff1a;默认是已经打开的主项目路径 ​​​​​​​BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SECRET_KEY 密钥 SECRET_KEY (dh&_fm2hfn9y)35!_6#$a7q%%^onoy#-a8x18r4(6*8f(aniDEBUG 帮助调试&#xff0c;默认…

【pip】本地和Anaconda的pip冲突时如何指定安装位置

输入指令&#xff1a; where pip 显示如下&#xff1a; D:\LenovoSoftstore\Anaconda\Scripts\pip.exe C:\python\python3.8\Scripts\pip.exe 可以看到有两个位置的pip&#xff0c;一个Anaconda下的pip&#xff0c;一个是本地的pip。 当我们使用pip安装的时候&#xff0c;系…

JavaScript综合练习3

JavaScript 综合练习 3 1. 案例演示 2. 代码实现 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewpor…

C语言操作符超详细总结

文章目录 1. 操作符的分类2. 二进制和进制转换2.1 2进制转10进制2.1.1 10进制转2进制数字 2.2 2进制转8进制和16进制2.2.1 2进制转8进制2.2.2 2进制转16进制 3. 原码、反码、补码4.移位操作符4.1 左移操作符4.2 右移操作符 5. 位操作符&#xff1a;&、|、^、~6. 逗号表达式…

【数据结构】堆(创建,调整,插入,删除,运用)

目录 堆的概念&#xff1a; 堆的性质&#xff1a; 堆的存储方式&#xff1a; 堆的创建 &#xff1a; 堆的调整&#xff1a; 向下调整&#xff1a; 向上调整&#xff1a; 堆的创建&#xff1a; 建堆的时间复杂度&#xff1a; 向下调整&#xff1a; 向上调整&#xff…

电商小程序05用户注册

目录 1 搭建页面2 设置默认跳转总结 我们上一篇拆解了登录功能&#xff0c;如果用户没有账号就需要注册了。本篇我们介绍一下注册功能的实现。 1 搭建页面 打开应用&#xff0c;点击左上角的新建页面 输入页面的名称&#xff0c;用户注册 删掉网格布局&#xff0c;添加表单容…

Cisco firepower2100系列使用FDM管理FTD

Cisco firepower2100系列使用FDM管理FTD 啥是FDM Firepower Device Manager 当思科Firepower系列运行的FTD镜像时&#xff0c;可以通过2种方式进行管理 第1种方式&#xff1a; FMC (Firepower management Center) 可以进行统一管理&#xff0c;一台FMC可以管理多个FTD&…

RK3568笔记十二:Zlmedia拉流显示测试

若该文为原创文章&#xff0c;转载请注明原文出处。 Zlmediakit功能很强大&#xff0c;测试一下拉流&#xff0c;在通过解码显示。 一、环境 1、平台&#xff1a;rk3568 2、开发板:ATK-RK3568正点原子板子 3、环境&#xff1a;buildroot 测试的代码在GitHub - airockchip/…

Stable Diffusion教程——使用TensorRT GPU加速提升Stable Diffusion出图速度

概述 Diffusion 模型在生成图像时最大的瓶颈是速度过慢的问题。为了解决这个问题&#xff0c;Stable Diffusion 采用了多种方式来加速图像生成&#xff0c;使得实时图像生成成为可能。最核心的加速是Stable Diffusion 使用了编码器将图像从原始的 3512512 大小转换为更小的 46…

91 xxl-job executor 还存在 并且 job 正在执行, 但是 job 被标记为 “任务结果丢失,标记失败“

前言 最近出现了一个这样的问题 我们生产环境中的一个 xxl-job 任务, 很大一部分执行记录被标记为 "任务结果丢失&#xff0c;标记失败", 几乎是 98% 吧 然后 调试的时候 存在几个令人疑惑的地方 1. 通过 xxl-job 点击查看任务的执行记录的日志, 日志为空, …

异步编程(JS)

前言 想要学习Promise&#xff0c;我们首先要了解异步编程、回调函数、回调地狱三方面知识&#xff1a; 异步编程 异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他事件做出反应而不必等待任务完成。 与此同时&#xff0c;你的程序也将在任务完成后显示…

《剑指 Offer》专项突破版 - 面试题 37 : 小行星碰撞(C++ 实现)

题目链接&#xff1a;LCR 037. 行星碰撞 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 输入一个表示小行星的数组&#xff0c;数组中每个数字的绝对值表示小行星的大小&#xff0c;数字的正负号表示小行星运动的方向&#xff0c;正号表示向右飞行&#xff0c;负…

【开源】SpringBoot框架开发医院门诊预约挂号系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 功能性需求2.1.1 数据中心模块2.1.2 科室医生档案模块2.1.3 预约挂号模块2.1.4 医院时政模块 2.2 可行性分析2.2.1 可靠性2.2.2 易用性2.2.3 维护性 三、数据库设计3.1 用户表3.2 科室档案表3.3 医生档案表3.4 医生放号…

【开源项目阅读】Java爬虫抓取豆瓣图书信息

原项目链接 Java爬虫抓取豆瓣图书信息 本地运行 运行过程 另建项目&#xff0c;把四个源代码文件拷贝到自己的包下面 在代码爆红处按ALTENTER自动导入maven依赖 直接运行Main.main方法&#xff0c;启动项目 运行结果 在本地磁盘上生成三个xml文件 其中的内容即位爬取…

Elasticsearch:通过 ingest pipeline 对大型文档进行分块

在我之前的文章 “Elasticsearch&#xff1a;使用 LangChain 文档拆分器进行文档分块” 中&#xff0c;我详述了如何通过 LangChain 对大的文档进行分块。那个分块的动作是通过 LangChain 在 Python 中进行实现的。对于使用版权的开发者来说&#xff0c;我们实际上是可以通过 i…

【工作学习 day04】 9. uniapp 页面和组件的生命周期

问题描述 uniapp常用的有&#xff1a;页面和组件&#xff0c;并且页面和组件各自有各自的生命周期函数&#xff0c;那么在页面/组件请求数据时&#xff0c;是用created呢&#xff0c;还是用onLoad呢&#xff1f; 先说结论: 组件使用组件的生命周期&#xff0c;页面使用页面的…

【Docker】02 镜像管理

文章目录 一、Images镜像二、管理操作2.1 搜索镜像2.1.1 命令行搜索2.1.2 页面搜索2.1.3 搜索条件 2.2 下载镜像2.3 查看本地镜像2.3.1 docker images2.3.2 --help2.3.3 repository name2.3.4 --filter2.3.5 -q2.3.6 --format 2.4 给镜像打标签2.5 推送镜像2.6 删除镜像2.7 导出…

移动应用开发Android 创建第一个Android项目

文章目录 一、创建第一个Android项目1.1 准备好Android Studio1.2 运行程序1.3 程序结构是什么app下的结构res - 子目录&#xff08;所有图片、布局、字AndroidManifest.xml 有四大组件&#xff0c;程序添加权限声明 Project下的结构 二、开发android时&#xff0c;部分库下载异…

svg基础(六)滤镜-图像,光照效果(漫反射,镜面反射),组合

1 feImage&#xff1a;图像滤镜 feImage 滤镜从外部来源取得图像数据&#xff0c;并提供像素数据作为输出&#xff08;意味着如果外部来源是一个 SVG 图像&#xff0c;这个图像将被栅格化。&#xff09; 1.1 用法: <feImage x"" y"" width"&quo…

基于鲲鹏服务NodeJs安装

准备工作 查看当前环境 uname -a查看鲲鹏云CPU架构 cat /proc/cpuinfo# 查看CPU architecture项&#xff0c;8表示v8&#xff0c;7表示v7下载Node.js NodeJs 选择 Linux Binaries (ARM) ARMv8 wget -c https://nodejs.org/dist/v12.18.3/node-v12.18.3-linux-arm64.tar.xz…
最新文章