每周小结003

NPC平滑运动

NPC被赋予一组路线比如List,NPC按照路线行进。当遇到转角时,如果直接改变朝向,NPC会显得过于程序化,必然是不可取的。该篇博客尝试了两种方法,分别是“转角圆弧”和“惰性转向”

1 转角圆弧

在转角时,根据预留量(可以设定与速度线性相关)提前脱离路径转弯,转弯的圆弧是两组边的内切圆。此时NPC的转弯角速度可以根据运动速度、预留量,转角的角度,精确求得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System.Collections.Generic;
using UnityEngine;

public class ArcMove : MonoBehaviour
{
public float TurnTriggerDistance = 5f; // 预先转向保留距离
public float MoveSpeed = 20f; // 匀速朝向前运动

private float _angularSpeed = 0f; // 本次转向的转向速度
private float _remainingTurnTime = 0f; // 本次转向的转向时间
private int _currentNodeIndex = 0;

private List<Vector3> _route = new()
{
new Vector3(10,0,-10),
new Vector3(-10,0,-10),
new Vector3(10,0, 0),
new Vector3(10,0,10),
new Vector3(-10,0,10),
};

private void Start() => ResetState();

void Update()
{
// ECS里的控制系统
var curPos = transform.position;
if (_remainingTurnTime <= 0 && _currentNodeIndex == _route.Count - 2)
{
var curPoint = _route[_currentNodeIndex];
var arrivePoint = _route[_currentNodeIndex + 1];
if (Vector3.Dot(arrivePoint - curPoint, arrivePoint - curPos) < 0)
ResetState();
}
if (_remainingTurnTime > 0)
{
_remainingTurnTime -= Time.deltaTime;
if (_remainingTurnTime <= 0) _angularSpeed = 0;
}
else
{
var haveNextPoint = _currentNodeIndex + 1 < _route.Count;
var haveNextNextPoint = _currentNodeIndex + 2 < _route.Count;
var handled = false;
if (haveNextNextPoint)
{
var curPoint = _route[_currentNodeIndex];
var nextPoint = _route[_currentNodeIndex + 1];
var nextNextPoint = _route[_currentNodeIndex + 2];
var inVector = nextPoint - curPoint;
var outVector = nextNextPoint - nextPoint;
var clampTurnTriggerDistance = Mathf.Min(TurnTriggerDistance, Mathf.Min(inVector.magnitude, outVector.magnitude) / 2);
var nowRemainDist = (curPos - nextPoint).magnitude;
if (nowRemainDist <= clampTurnTriggerDistance)
{
++_currentNodeIndex;
handled = true;
// 脱离
var angle = Vector3.SignedAngle(inVector.normalized, outVector.normalized, Vector3.up);
var absoluteAngle = Mathf.Abs(angle);
if (absoluteAngle < 1f) return; // 角度太小,Tan会很大,安全起见,直接当直线跑
var inCircleRadius = nowRemainDist / Mathf.Tan(absoluteAngle * Mathf.Deg2Rad / 2);
var arcLength = 2 * Mathf.PI * inCircleRadius * absoluteAngle / 360;
_remainingTurnTime = arcLength / MoveSpeed;
_angularSpeed = angle / _remainingTurnTime;
}
}
if (haveNextPoint && !handled)
{
var curDir = transform.rotation * Vector3.forward;
var targetDir = _route[_currentNodeIndex + 1] - curPos;
var diffAngle = Vector3.SignedAngle(curDir.normalized, targetDir.normalized, Vector3.up);
_angularSpeed = diffAngle * MoveSpeed; // 朝向收敛与速度正相关
}
}

// ECS里的Movement系统
transform.rotation *= Quaternion.Euler(0, _angularSpeed * Time.deltaTime, 0);
transform.position = transform.position + MoveSpeed * Time.deltaTime * transform.forward;
}

private void ResetState()
{
_currentNodeIndex = 0;
transform.SetPositionAndRotation(_route[_currentNodeIndex], Quaternion.LookRotation(_route[1] - _route[0]));
}
}

2 惰性转向

NPC如果只有起点和终点,当抵达终点后再赋予他新的终点。简单描述:NPC随机选择路,不走回头路。
该方法的措施是:每一刻都会计算NPC与终点的目标方向、与当前边的回归方向,得出最终的期望方向,通过NPC朝向和期望方向,最终算出此时的转向角。如果希望NPC快速回归到边,可以增大回归方向。如果希望NPC快速完成转向,可以增大最终求得的转向角。

任意时刻NPC转向角获取示意图

任意时刻NPC转向角获取示意图

1
2
3
4
5
6
7
8
9
10
11
12
var curPos = transform.Position;
var curDir = transform.Rotation * Vector3.forward;

var pathDir = (data.PrevPos - data.NextPos).normalized; // 路径方向: Prev -> Next
var toNext = curPos - data.NextPos; // 目标点指向当前位置的向量
var projectedLength = Vector3.Dot(pathDir, toNext); // 目标点指向当前位置的向量在路径方向上的投影长度
var projectedOnPath = pathDir * projectedLength; // 路径方向的投影向量
var lateralOffset = projectedOnPath - toNext; // 偏离路径的横向偏移

var desiredDir = data.NextPos - curPos + lateralOffset * _PathRecoverStrength;
var diffAngle = Vector3.SignedAngle(curDir.normalized, desiredDir.normalized, Vector3.up);
data.AngularSpeed = diffAngle * data.MoveSpeed; // 朝向收敛与速度正相关

代码如上,每帧会按照转向角进行方向修正。