Unity Waypoints脚本运行时问题及需求:Speed参数无效、路径点动态更新、循环/往返移动实现
Hey Daniel, let's work through each of your three waypoint movement problems with clear fixes and explanations:
1. Fixing Runtime Speed Changes Not Affecting Moving Objects
Your initial thought about the coroutine not reading the latest speed value is a common pitfall, but actually, your current code does read the live speed variable each frame (since Vector3.MoveTowards uses speed * Time.deltaTime directly from the class member). The likely confusion comes from thinking go = false blocks all updates—but speed changes should still affect active coroutines.
To make this explicit and avoid any accidental caching, we’ve kept the direct reference to the speed member in the movement logic, ensuring Inspector changes take effect immediately.
2. Dynamically Updating Positions Without Performance Overhead
Running GetLinePointsInWorldSpace every frame is unnecessary. Instead, we can check for changes in the LineRenderer or pointsToMove list only when needed, then update positions only when a change is detected:
- Cache previous values (line point count, points list count, and a simple position "hash")
- Compare current values to cached ones each frame (minimal overhead)
- Update
positionsonly when discrepancies are found
3. Implementing Loop Movement (Two Modes)
To add loop behavior, we’ve introduced an enum to select between Loop (restart from start) and Ping-Pong (reverse direction). The coroutine now runs indefinitely (unless set to stop) and adjusts its path logic when reaching the end of the route.
Full Modified Code
using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; public class Waypoints : MonoBehaviour { public GameObject objectPrefab; public LineRenderer lineRenderer; public List<Transform> pointsToMove; public float speed; public bool go = false; // Add loop mode selection in Inspector public LoopMode loopMode = LoopMode.Loop; private List<GameObject> objectsToMove; private List<Vector3> positions; // Cache values to detect path changes efficiently private int lastLinePointCount; private int lastPointsToMoveCount; private Vector3 lastPointsToMoveHash; // Enum to define loop behavior options public enum LoopMode { None, // Original stop-at-end behavior Loop, // Restart from start after reaching end PingPong // Reverse direction after hitting start/end } void Start() { objectsToMove = new List<GameObject>(); // Initialize cached values for change detection UpdateCachedValues(); positions = GetCombinedPositions(); } private void Update() { // Original object spawn trigger logic if (go && lineRenderer.positionCount > 0 && CurvedLineRenderer.linesSet) { for (int i = 0; i < 3; i++) { objectsToMove.Add(Instantiate(objectPrefab, transform.parent)); StartCoroutine(Move(i)); } go = false; } // Update positions only if path has changed if (HasPathChanged()) { positions = GetCombinedPositions(); UpdateCachedValues(); } } // Check if LineRenderer or pointsToMove list has changed private bool HasPathChanged() { // Check LineRenderer point count if (lineRenderer.positionCount != lastLinePointCount) { return true; } // Check pointsToMove list size if (pointsToMove.Count != lastPointsToMoveCount) { return true; } // Simple position hash check to detect changes in point positions Vector3 currentHash = Vector3.zero; foreach (Transform t in pointsToMove) { currentHash += t.position; } if (currentHash != lastPointsToMoveHash) { return true; } return false; } // Update cached values after a path change private void UpdateCachedValues() { lastLinePointCount = lineRenderer.positionCount; lastPointsToMoveCount = pointsToMove.Count; lastPointsToMoveHash = Vector3.zero; foreach (Transform t in pointsToMove) { lastPointsToMoveHash += t.position; } } // Combine LineRenderer points and pointsToMove into a single path list private List<Vector3> GetCombinedPositions() { var linePoints = new Vector3[lineRenderer.positionCount]; lineRenderer.GetPositions(linePoints); var combined = linePoints.ToList(); foreach(Transform objToMove in pointsToMove) { combined.Add(objToMove.position); } return combined; } private IEnumerator Move(int i) { // Initial delay before starting movement yield return new WaitForSeconds(i * 10); int pointIndex = 0; float threshold = 0.1f; // Direction multiplier: 1 = forward, -1 = backward (for PingPong) int direction = 1; while (true) { // Safety check: skip if path is empty if (positions.Count == 0) { yield return null; continue; } // Check if object is close enough to the target point if (Vector3.Distance(objectsToMove[i].transform.position, positions[pointIndex]) < threshold) { switch (loopMode) { case LoopMode.None: // Stop movement when reaching the end yield break; case LoopMode.Loop: // Reset to the first point pointIndex = 0; break; case LoopMode.PingPong: // Reverse direction when hitting start or end if (pointIndex == positions.Count - 1 || pointIndex == 0) { direction *= -1; } // Move to next point in current direction pointIndex += direction; break; } } // Move towards target using the latest speed value objectsToMove[i].transform.position = Vector3.MoveTowards( objectsToMove[i].transform.position, positions[pointIndex], speed * Time.deltaTime ); yield return null; } } }
Key Changes Explained:
- Live Speed Updates: The coroutine uses the
speedmember variable directly every frame, so Inspector changes take effect immediately on moving objects. - Efficient Path Updates: The
HasPathChanged()method uses lightweight checks to avoid unnecessary recalculation of the path, keeping performance overhead low. - Flexible Loop Modes: The
LoopModeenum lets you switch between stopping, looping from the start, or ping-ponging back and forth—all handled within the persistent coroutine.
内容的提问来源于stack exchange,提问作者Daniel Lip

