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

【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

核心代码:

    // 目标行星Transformpublic 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方法,效果如文章开头的视频中所示)
请添加图片描述

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

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

吉祥话

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

相关文章:

  • 8.0 Zookeeper 四字命令教程详解
  • 使用x86架构+Nvidia消费显卡12G显存,搭建智能终端,将大模型本地化部署,说不定是未来方向,开源交互机器人设计
  • 寒假作业-day5
  • Spring的学习(上)
  • 性能评测|虚拟化和裸金属 K8s 哪个性能更好?
  • C++:二叉搜索树模拟实现(KV模型)
  • 计算机网络相关题目及答案(第六章)
  • 2月6日作业
  • HARRYPOTTER: FAWKES
  • 在angular12中proxy.conf.json中配置详解
  • 线性矩阵不等式LMI与李雅普诺夫Lyapunov稳定性
  • 时序预测 | MATLAB实现基于CNN-GRU-AdaBoost卷积门控循环单元结合AdaBoost时间序列预测
  • 数据结构-->线性表-->单链表
  • 【Java数据结构】单向 不带头 非循环 链表实现
  • 使用 Python、Elasticsearch 和 Kibana 分析波士顿凯尔特人队
  • HTTP中GET与POST的区别 99%的错误认识
  • Java 网络编程(2):UDP 的使用
  • Laravel 中的一个后期静态绑定
  • React as a UI Runtime(五、列表)
  • React-Native - 收藏集 - 掘金
  • Spring Security中异常上抛机制及对于转型处理的一些感悟
  • v-if和v-for连用出现的问题
  • 创建一种深思熟虑的文化
  • 纯 javascript 半自动式下滑一定高度,导航栏固定
  • 记一次和乔布斯合作最难忘的经历
  • 快速体验 Sentinel 集群限流功能,只需简单几步
  • 前端代码风格自动化系列(二)之Commitlint
  • 微信支付JSAPI,实测!终极方案
  • 新书推荐|Windows黑客编程技术详解
  • 学习JavaScript数据结构与算法 — 树
  • 用Visual Studio开发以太坊智能合约
  • 怎样选择前端框架
  • 数据库巡检项
  • ​HTTP与HTTPS:网络通信的安全卫士
  • ​MPV,汽车产品里一个特殊品类的进化过程
  • #我与虚拟机的故事#连载20:周志明虚拟机第 3 版:到底值不值得买?
  • (4.10~4.16)
  • (十一)手动添加用户和文件的特殊权限
  • (一)Java算法:二分查找
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • (转)IOS中获取各种文件的目录路径的方法
  • ./configure,make,make install的作用
  • .net web项目 调用webService
  • .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)
  • :=
  • @Autowired多个相同类型bean装配问题
  • @javax.ws.rs Webservice注解
  • [20180224]expdp query 写法问题.txt
  • [202209]mysql8.0 双主集群搭建 亲测可用
  • [Android]Android开发入门之HelloWorld
  • [Angular 基础] - 表单:响应式表单
  • [BUAA软工]第一次博客作业---阅读《构建之法》
  • [codevs 1296] 营业额统计
  • [CQOI 2011]动态逆序对
  • [DL]深度学习_Feature Pyramid Network