最近吃鸡游戏火啊,至今也吃了好几晚的鸡了,无奈手雷就是丢不准,从窗户丢雷丢几个弹出几个,各种误伤自己人……而别人家的手雷:
一般的游戏里手雷都是盲投,不过一般游戏也不会对弹道有这么精确的要求,尽管往敌人家里丢就好了,能不能炸到人全靠缘分。那么,如果把雷精确的从窗户丢进去呢,不不不,是如何在Unity里实现手雷的轨迹,从而预判手雷落点呢,今天我们就来讨论这个问题!
一、轨迹绘制
众所周知,手雷的弹道其实是一个抛物线,扔手雷的过程,其实就是给一个物体以一定的初速度的自由落体运动,在运动过程中没有碰倒任何物体前只受重力和空气阻力。这里我们不考虑空气阻力,那么快速来写一下轨迹方程:
velocity += Vector3.down * Gravity * Time;
position += velocity * Time;
其中velocity是矢量,表示当前运动速度,变量类型为为Vector3;Gravity表示重力大小,乘以(0, -1, 0),表示重力矢量;最终得到当前手雷运动的位置Position。
OK,到这一步很简单,那么接下来的问题是:如何将这个抛物线绘制出来?
这里我们选择用网格绘制。要绘制抛物线,我们需要先对轨迹进行预模拟,记录抛物线上的点,然后将这些点连成线,再将线连面。首先,预模拟一次抛物线,并记录一定时间间隔(Interval)的点的位置:
int PointsCount = 100;
List<Vector3> Points = new List<Vector3>();
for (int i = 0; i < PointsCount; i++)
{
Points.Add(pos);
velocity += Vector3.down * Gravity * Interval;
pos += velocity * Interval;
}
然后另写一个类,用我们记录的点生成Mesh:
using System.Collections.Generic;
using UnityEngine;
public class MeshData
{
public Vector3 Up = Vector3.up;
private Vector3[] vertices;
private int[] triangles;
private Vector2[] uvs;
private int vertexIndex;
private int triangleIndex;
private float Width;
private List<Vector3> Line;
public MeshData(List<Vector3> line, float width)
{
Line = line;
Width = width;
vertices = new Vector3[Line.Count * 2];
uvs = new Vector2[Line.Count * 2];
triangles = new int[(Line.Count - 1) * 6];
vertexIndex = triangleIndex = 0;
int length = Line.Count;
for (int i = 0; i < length; i++) {
vertices[vertexIndex] = Line[i] + Up * Width;
vertices[vertexIndex + length] = Line[i] - Up * Width;
uvs[vertexIndex] = new Vector2(i / (float)length, 0);
uvs[vertexIndex + length] = new Vector2(i / (float)length, 1);
if (i < length - 1) {
AddTriangle(vertexIndex, vertexIndex + length + 1, vertexIndex + length);
AddTriangle(vertexIndex + length + 1, vertexIndex, vertexIndex + 1);
}
vertexIndex++;
}
}
public Mesh CreateMesh()
{
Mesh mesh = new Mesh();
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.uv = uvs;
// mesh.RecalculateNormals();
return mesh;
}
void AddTriangle(int a, int b, int c)
{
triangles[triangleIndex] = a;
triangles[triangleIndex + 1] = b;
triangles[triangleIndex + 2] = c;
triangleIndex += 3;
}
}
这里点的顺序是按行来的,用AddTriangle()方法来记录三角形绘制顺序,抛物线连线会按宽度(Width)来复制一份出来,这两条线形成的面就是最终的Mesh,面的朝向可以通过修改Up来做调整,最后的法线计算可以按需求来打开注释,我这里表示不需要,去掉不必要的计算。
有了Mesh,赶快把Mesh赋值给一个MeshFilter来看看效果吧:
MeshData data = new MeshData(Points, Width);
TrackRender.mesh = data.CreateMesh();
可以看到,It looks pretty good!这里,我希望可以更清楚的对比当前网格的密度和效果,添加了一小段代码对其进行编辑器扩展:
void OnSceneGUI()
{
Handles.color = Color.green;
for (int i = 0, length = track.Points.Count; i < length; i++)
{
Handles.SphereHandleCap(0, track.Points[i], Quaternion.identity, 1, EventType.Repaint);
}
}
小球表示间隔,效果如下:
二、落点位置
现在轨迹有了,那么接下来的问题是,轨迹不可能无限延长吧,那么终点在哪,改如何获取?
这里我用一个笨办法,用射线检测来做(当然也是因为我没有想到其他更好的办法,orz),两点之间做射线,如果有碰到碰撞层(提前设好碰撞层级),就代表到了轨迹的终点。好了,有了思路,代码极其简单:
Vector3 endPos = Vector3.zero;
RaycastHit hitInfo;
for (int i = 0; i < PointsCount; i++)
{
Points.Add(pos);
if (i != 0 && i%RayCastSimplify == 0)
{
Vector3 dirVec = pos - Points[i - RayCastSimplify];
if (Physics.SphereCast(Points[i - RayCastSimplify], RaycastRadius, dirVec.normalized, out hitInfo, dirVec.magnitude, mask.value))
{
endPos = hitInfo.point;
break;
}
}
velocity += Vector3.down * Gravity * Interval;
pos += velocity * Interval;
}
每次记录位置时,两点之间做射线检测,如果有碰撞,则返回,并记录终点位置endPos。最后给一张合适的贴图,并在endPos再放一个合适的圈表示落点即可(我比较懒,只给了一张圆),最终效果:
OK,到这里,我们的手雷弹道功能基本完成啦!可以看到,由于落点和地面贴的太近,落点会出现闪烁情况,当然,这里只提供基本思路,剩下的完善与优化就交在你的手里了。今天就到这里,如果你有更好的解决方案,欢迎交流讨论!