总之,为了实现“让小球能按照既定轨迹运动”这种蛋疼又基础的功能,我又跑来写笔记了。
在Unity里其实已经有很多前辈做过了路点系统这个东西,比如AssetStore里的SimpleWaypointSystem(https://assetstore.unity.com/packages/tools/animation/simple-waypoint-system-2506)和StandardAssets的Utility中的WaypointCircult,
这些都是现成可直接使用的轮子。
我在使用上述两个系统的时候,发现它们的确很好用。但是前者插件资源过多,我不喜欢丢这么大一个插件包在工程里,后者内部结构复杂且只能在编辑器里动态定义路径(Editor还疯狂报红错),总之就是用的都不咋顺心 _(:з)∠)_ 于是乎看了这两套代码后我觉得路点系统这个东西只从概念上来看并不复杂,索性就自己写一个好了。
因为笔者的注释习惯还行,代码会在文章最后直接放出。对于这个常用又简单的系统而言,我再多嘴两个常用操作:
平滑路径
我们打了一串的路点后,若是直接用直线把这些路点按顺序连接,不仅视觉上不好看,在试用上也不方便拟合一些弯道情景。因此现在通用的路径系统多会根据三次Beizer曲线方程:
Bn(t)=P0(1−t)3+3P1t(1−t)2+3P2t2(1−t)+P3t3,tϵ[0,1]Bn(t)=P0(1−t)3+3P1t(1−t)2+3P2t2(1−t)+P3t3,tϵ[0,1] (2),把过路点的特征曲线作为路径。这个做法的大致效果如图:
这个方程代码实现我扒了SimpleWaypointSystem内的函数,并且看他们起的注释这也是从HOTween的路径代码里扒出来的,我回头看了眼StandardAssets里的函数,用的方程也一毛一样,总之…大家用的都是这个方程,代码化咱也搞不出花样了,就直接复制了。
路径采点
照搬WaypointCircul的把实际数据点用数据存下的思路,为方便进行长度查询,我在路径曲线定义时会根据平滑度系数把曲线的总划分段数记下。这样,我就能明确的直到我的追随点目前在曲线的几分之几的段落上,可以通过直接改变其所处段数表示它在路径上的位置。
代码实现
路径(路点系统):
using System; using System.Collections.Generic; using UnityEngine; /// <summary> /// way path /// </summary> public class WayPath : MonoBehaviour { public List<Vector3> anchors = new List<Vector3>(); Vector3[] curveAnchors; public int PointCount; public Action PathChanged;//path curve changed /// <summary> /// max anchor count /// </summary> public int MaxAnchor = 10; /// <summary> /// smoothy count between anchors /// </summary> public int SmoothyBetweenPoints = 20; [ContextMenu("WayPointsFromChildren")] public void WayPointsFromChildren() { anchors.Clear(); for (int i = 0; i < transform.childCount; i++) anchors.Add(transform.GetChild(i).position); FixedCurve(); } [ContextMenu("WayPointsFromMesh")] public void WayPointsFromMesh() { MeshFilter meshfilter = GetComponent<MeshFilter>(); if (meshfilter == null) return; Mesh mesh = meshfilter.sharedMesh; if (mesh == null) return; anchors.Clear(); anchors.AddRange(mesh.vertices); FixedCurve(); } /// <summary> /// add anchor at tail of curve /// </summary> /// <param name="points"></param> public void AddPoints(params Vector3[] points) { anchors.AddRange(points); FixedCurve(); } /// <summary> /// clear anchors and reset them by new points. /// </summary> /// <param name="points"></param> public void Reset(params Vector3[] points) { anchors.Clear(); anchors.AddRange(points); FixedCurve(); } /// <summary> /// clear anchors and curve /// </summary> public void Clear() { anchors.Clear(); FixedCurve(); } /// <summary> /// Get point on curve by index. /// </summary> /// <param name="index"></param> /// <returns></returns> public Vector3 GetPoint(int index) { if (index < 0) index = 0; //empty if (anchors.Count < 1) { return Vector3.zero; } //line else if (anchors.Count < 3) { index %= anchors.Count; return anchors[index]; } //points >= 3, can smooth to curve else { int totalCount = anchors.Count * SmoothyBetweenPoints; index %= totalCount; if (curveAnchors == null) FixedCurve(); return Lerp(curveAnchors, 1.0f * index / totalCount); } } /// <summary> /// Get nearist point on curve. /// </summary> /// <param name="point"></param> /// <returns></returns> public int GetNearistIndex(Vector3 point) { //empty if (anchors.Count < 1) { return 0; } //line else if (anchors.Count < 3) { float dis = Mathf.Infinity; int index = 0; for (int i = 0; i < anchors.Count; i++) { float dist = Vector3.Distance(point, anchors[i]); if (dist < dis) { dis = dist; index = i; } } return index; } //points > 2, can smooth to curve else { float dis = Mathf.Infinity; int index = 0; if (curveAnchors == null) FixedCurve(); for (int i = 0; i < PointCount; i++) { Vector3 tmppoint = Lerp(curveAnchors, 1.0f * i / PointCount); float dist = Vector3.Distance(point, tmppoint); if (dist < dis) { dis = dist; index = i; } } return index; } } /// <summary> /// Build Beizer Curve by new anchors. /// </summary> private void FixedCurve() { //fix anchor range if (anchors.Count > MaxAnchor) { int range = anchors.Count - MaxAnchor; anchors.RemoveRange(0, range); } PointCount = anchors.Count > 2 ? anchors.Count * SmoothyBetweenPoints : anchors.Count; if (anchors.Count > 1) { curveAnchors = new Vector3[anchors.Count + 2]; //Extension points curveAnchors[0] = anchors[0] + anchors[0] - anchors[1]; curveAnchors[anchors.Count + 1] = anchors[anchors.Count - 1] + anchors[anchors.Count - 1] - anchors[anchors.Count - 2]; for (int i = 0; i < anchors.Count; i++) { curveAnchors[i + 1] = anchors[i]; } } if (PathChanged != null) PathChanged.Invoke(); } /// <summary> /// Gets the point on the curve at a given percentage (0-1). Taken and modified from HOTween. /// <summary> //http://code.google.com/p/hotween/source/browse/trunk/Holoville/HOTween/Core/Path.cs private static Vector3 Lerp(Vector3[] pts, float t) { int numSections = pts.Length - 3; int currPt = Mathf.Min(Mathf.FloorToInt(t * (float)numSections), numSections - 1); float u = t * (float)numSections - (float)currPt; Vector3 p0 = pts[currPt]; Vector3 p1 = pts[currPt + 1]; Vector3 p2 = pts[currPt + 2]; Vector3 p3 = pts[currPt + 3]; return 0.5f * ( (-p0 + 3f * p1 - 3f * p2 + p3) * (u * u * u) + (2f * p0 - 5f * p1 + 4f * p2 - p3) * (u * u) + (-p0 + p2) * u + 2f * p1 ); } }
使用情景:
初始化路点渲染插件:
#if UNITY_EDITOR using System.Collections.Generic; using UnityEngine; /// <summary> /// Uses Unity's LineRenderer component to render paths. /// <summary> [RequireComponent(typeof(WayPath))] public class WayPathRenderer : MonoBehaviour { /// <summary> /// Spacing between LineRenderer positions on the path. /// <summary> public float spacing = 0.05f; [SerializeField] Color SelectedColor = Color.yellow; [SerializeField] Color LineColor = new Color(255, 255, 0, 0.5f); WayPath path; //updates LineRenderer positions void OnDrawGizmosSelected() { Gizmos.color = SelectedColor; DrawPath(); } void OnDrawGizmos() { Gizmos.color = LineColor; DrawPath(); } void DrawPath() { path = GetComponent<WayPath>(); if (path == null) return; //set initial size based on waypoint count if (path.PointCount > 0) { //draw line Vector3 lastpos = path.GetPoint(0); for (int i = 1; i < path.PointCount; i++) { Vector3 point = path.GetPoint(i); Gizmos.DrawLine(lastpos, point); lastpos = point; } //draw anchor for (int i = 0; i < path.anchors.Count; i++) { Gizmos.DrawWireSphere(path.anchors[i], 1); } } } } #endif
路径渲染
路径跟随插件:
using System; using UnityEngine; public class WaypathTracker : MonoBehaviour { // This script can be used with any object that is supposed to follow a // route marked out by waypoints. public WayPath path; // A reference to the waypoint-based route we should follow // If loop track public Transform target; public float targetStep = 10.0f; public float trackDist = 10.0f; public bool isLooping = true; public int curPathIndex { get; private set; } // The progress round the route, used in smooth mode. private int pathLength = 0;//total index count // setup script properties private void Start() { // the point to aim position on curve if (target == null) { target = new GameObject(name + " Waypoint Target").transform; } // init relations of path SwitchPath(path); // interate Reset(); } private void OnDestroy() { if (path != null) path.PathChanged -= Reset; path = null; } // reset the object to sensible values public void Reset() { if (path == null) return; pathLength = path.PointCount; curPathIndex = path.GetNearistIndex(transform.position); FixedUpdate(); } /// <summary> /// switch to another followed path /// </summary> /// <param name="newpath"></param> public void SwitchPath(WayPath newpath) { if (path == newpath) return; if (path != null) path.PathChanged -= Reset; path = newpath; if (path == null) return; path.PathChanged += Reset; Reset(); } private void FixedUpdate() { //if change target if (path == null) return; //Get target point target.position = path.GetPoint(curPathIndex); //If reach the radius within the path then move to next point in the path if (Vector3.Distance(transform.position, target.position) < trackDist) { //step move next track index Vector3 checkpos = target.position; while (Vector3.Distance(target.position, checkpos) < targetStep) { curPathIndex += 1; // determine the position we should currently be aiming for if (curPathIndex >= pathLength)//if reached end point { if (isLooping) { curPathIndex = 0;//return index to path head } else { curPathIndex = pathLength - 1; return;//Don't move the vehicle if path is finished } } checkpos = path.GetPoint(curPathIndex); } } } private void OnDrawGizmos() { if (Application.isPlaying) { Gizmos.color = Color.green; Gizmos.DrawLine(transform.position, target.position); Gizmos.DrawWireSphere(target.position, 1); Gizmos.color = Color.yellow; Gizmos.DrawLine(target.position, target.position + target.forward); } } }
使用情景:
白色小球对路径进行采点