【Unity】万有引力和轨道计算(1)

这算是一个忙了大半年的比赛项目了,终于完成了来补个文章Orz

先上效果:

万有引力

首先要让星体之间能相互吸引,互相施加一个万有引力。这个我实现起来就很粗暴了,直接用过刚体施加一个力:

代码语言:javascript
复制
//计算万有引力数值
private float CalculateGravityModulus(float targetMass, float distance)
{
   return PhysicBase.GetG() * (GetMass() * targetMass / (distance * distance));
}

//计算万有引力
private Vector3 GetGravityVector3(Rigidbody rigidbody)
{
var distance = Vector3.Distance(transform.position, rigidbody.position);
var normalizedDirection = (transform.position - rigidbody.position).normalized;
return CalculateGravityModulus(rigidbody.mass, distance) * normalizedDirection;
}

不过由于一个星球会受到好几个其他星球的影响,这里建了一个List来计算合力,用Trigger来判断是否计算两个星球之间的引力:

代码语言:javascript
复制
//     对之产生影响的星球
public List<AstralBody> affectedPlanets = new List<AstralBody>();

private void OnTriggerEnter(Collider other)
{
var astral = other.GetComponent<AstralBody>();
if (astral != null &&
!other.isTrigger &&
enableAffect &&
!astral.banAffectedPlanets.Contains(this) &&
!astral.affectedPlanets.Contains(this))
astral.affectedPlanets.Add(this);
}

private void OnTriggerExit(Collider other)
{
var astral = other.GetComponent<AstralBody>();
if (astral != null && enableAffect && astral.affectedPlanets.Contains(astral))
astral.affectedPlanets.Remove(this);
}

//计算受力
public Vector3 CalculateForce()
{
var forceResult = new Vector3(0, 0, 0);
foreach (var astralBody in affectedPlanets)
forceResult += astralBody.GetGravityVector3(astralBodyRigidbody);

  return forceResult;

}

然后直接暴力运算:

代码语言:javascript
复制
protected virtual void FixedUpdate()
{
if (!astralBodyRigidbody.isKinematic) lastVelocity = astralBodyRigidbody.velocity;
astralBodyRigidbody.AddForce(CalculateForce());
}

效果还行:


轨道预测

接下来就是重点了,怎么根据已知星球信息推出它在固定时间之后的位置呢?

我这里的解决方案是,将所有的星球的质量、速度、坐标、影响星球各种信息存在一起,预测时做这样的计算:

  1. 根据星球各自的位置和当前速度,用匀速直线运动近似,计算其在n秒后所处的位置。
  2. 根据每个星球各自的坐标和质量计算它们各自当前受力。
  3. 用加速度公式计算星球在进行前面那段位移后速度的改变量,得出新速度。
  4. 重复上面三步。
代码语言:javascript
复制
//开始采样
for (var i = 0; i < sample; i++)
//遍历星体
{
foreach (var astralBody in _astralBodies)
{
//加速度
/*
* F=ma
* delta v=at
* s = vt + 0.5a(t^2)
*/
var acceleration = CalculateForce(astralBody, i, astralBodyMasses) / astralBodyMasses[astralBody];

    _orbitPoints[astralBody].Add(_orbitPoints[astralBody].Last()               +
                                 astralBodyVelocities[astralBody] * _deltaTime +
                                 .5f                              * acceleration * Mathf.Pow(_deltaTime, 2));
    //加速后速度
    astralBodyVelocities[astralBody] += acceleration * _deltaTime;
    
}

}

这部分计算如果全部放到主线程做真的就挺卡的,这里做了个多线程:

代码语言:javascript
复制
//引力步进
private void TraceGravity()
{
var astralBodyVelocities = new Dictionary<ITraceable, Vector3>();
foreach (var astralBody in _astralBodies)
{
astralBodyVelocities[astralBody] = astralBody.GetVelocity();
//起始点改为当前位置
if (_orbitPoints.ContainsKey(astralBody)) _orbitPoints[astralBody].Clear();
_orbitPoints[astralBody].Add(astralBody.GetPosition());
}
var astralBodyMasses = new Dictionary<ITraceable, float>();
_astralBodies.ForEach(a => astralBodyMasses.Add(a, a.GetMass()));
StartNewThread(new[]
{
astralBodyVelocities,
(object) astralBodyMasses
});
}

private void StartNewThread(object[] dict)
{
_thread = new Thread(Sample);
_thread.Start(dict);
}

private void Sample(object objs)
{
var dicts = (object[]) objs;
var astralBodyVelocities = (Dictionary<ITraceable, Vector3>) dicts[0];
var astralBodyMasses = (Dictionary<ITraceable, float>) dicts[1];
//开始采样
for (var i = 0; i < sample; i++)
//遍历星体
{
foreach (var astralBody in _astralBodies)
{
//加速度
/*
* F=ma
* delta v=at
* s = vt + 0.5a(t^2)
*/
var acceleration = CalculateForce(astralBody, i, astralBodyMasses) / astralBodyMasses[astralBody];
_orbitPoints[astralBody].Add(_orbitPoints[astralBody].Last() +
astralBodyVelocities[astralBody] * _deltaTime +
.5f * acceleration * Mathf.Pow(_deltaTime, 2));
//加速后速度
astralBodyVelocities[astralBody] += acceleration * _deltaTime;
}
}
_actionTypes.Add(ActionType.Finished);
}

路径显示

显示其实挺容易的,用个LineRenderer把坐标全都放进去就可以了。后来发现了一个叫Dreamteck Splines的插件可以做自动平滑,输进去的点能少一些,之后在做轨道拟合的时候用的还挺爽的,不过当时工期紧张之前的代码就没有重构233

这里随便写了个Shader来做线的显示:

代码语言:javascript
复制
Shader "Line/UILine"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
[HDR]_Color("Color",Color) = (0,0,0,0)
_Speed ("Speed",Range(0,1)) = .5
_Density("Density",float) = 30
}
SubShader
{
Tags
{
"RenderType"="Transparent" "IgnoreProjector"="True" "Queue"="Transparent"
}
ZTest Off
LOD 200
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma surface surf NoLight vertex:vert alpha noforwardadd

    float4 LightingNoLight(SurfaceOutput s, float3 lightDir, half3 viewDir, half atten)
    {
        float4 c;
        c.rgb = s.Albedo;
        c.a = s.Alpha;
        return c;
    }

    sampler2D _MainTex;
    fixed4 _SelfCol;
    fixed4 _Color;

    fixed _Speed;
    fixed _Density;


    struct Input
    {
        float2 uv_MainTex;
        float4 vertColor;
    };

    void vert(inout appdata_full v, out Input o)
    {
        o.vertColor = v.color;
        o.uv_MainTex = v.texcoord;
    }

    void surf(Input IN, inout SurfaceOutput o)
    {
        fixed2 uv = float2(IN.uv_MainTex.x  * _Density ,IN.uv_MainTex.y) + float2((IN.uv_MainTex.x- _Time.y * _Speed )+1,0);
        half4 c = tex2D(_MainTex, frac(uv));
        o.Alpha = IN.vertColor.a * c.a;
        o.Albedo = c * _Color;
    }
    ENDCG
}
FallBack &#34;Diffuse&#34;

}

效果:

多个相互影响的星球放在一起也没有问题:

项目仓库:

HkingAuditore/Kepler