core/npc/NpcMovement.cs
public class NpcMovement : Component {
[Property] public Vector3 Goal {get; set;}
public Action OnReachTarget {get; set;}
[Property] public Vector3 OnReachTargetPosition {get; set;}
[Button] public bool Go(bool force = true) {
Goal = Grid.Snap(Goal);
if (Path.Count > 0) {
Path.Clear();
WaitingForGo = true;
HitGo = 0.1f;
return false;
}
Path.Clear();
if (Grid.IsBlocked(Goal)) {
if (Goal == OnReachTargetPosition)
OnReachTarget.Invoke();
return false;
}
Path.Add(Grid.Snap(WorldPosition));
Advance(new());
if (Path.Count == 0) {
if (Goal == OnReachTargetPosition)
OnReachTarget.Invoke();
return false;
}
if (!Path.Last().AlmostEqual(Goal)) {
if (Goal == OnReachTargetPosition)
OnReachTarget.Invoke();
Path.Clear();
return false;
}
//DebugOverlay.Line(Path, Color.Red, 5);
Run = LastGo < 0.4f;
LastGo = 0;
return true;
}
[Property, ReadOnly] public bool WaitingForGo {get ; set;}
private TimeSince LastGo {get; set;} = 1000;
private TimeUntil HitGo {get; set;}
[Property] public bool DontUpdateActivity {get; set;}
[Property, ReadOnly] public bool Run {get; set;} = false;
[Property, ReadOnly] public List<Vector3> Path {get; set;} = new();
public void Advance(List<Vector3> invalid, bool useangle = false, float angle = 0f) {
if (Path.Last() == Goal)
return;
if (useangle) {
var canidate = Grid.Snap(Path.Last() + Rotation.FromYaw(angle).Forward * 30f);
if (Grid.IsBlocked(canidate) || Path.Last().Distance(Goal) <= canidate.Distance(Goal) + Path.Last().Distance(canidate) * 0.5f)
useangle = false;
}
if (!useangle) {
var best = -1f;
for (int i = 0; i < 6; i++) {
var canidate = Grid.Snap(Path.Last() + Rotation.FromYaw(i * 60f - 30f).Forward * 30f);
if (Grid.IsBlocked(canidate))
continue;
var dot = Vector3.Direction(Path.Last(), canidate).Dot(Vector3.Direction(Path.Last(), Goal));
if (dot < best)
continue;
angle = i * 60f - 30f;
best = dot;
}
}
var point = Grid.Snap(Path.Last() + Rotation.FromYaw(angle).Forward * 30f);
Path.Add(point);
if (Path.Count > 40)
return;
Advance(invalid, true, angle);
}
public Vector3 EntityRoot {get; set;}
protected override void OnUpdate() {
if (WaitingForGo && HitGo) {
Go();
WaitingForGo = false;
}
if (!Grid.Snap(OnReachTargetPosition).AlmostEqual(Grid.Snap(Goal)))
OnReachTarget = null;
if (!WaitingForGo && Path.Count == 0 && Grid.Snap(EntityRoot).AlmostEqual(Grid.Snap(Goal)))
OnReachTarget?.Invoke();
base.OnUpdate();
var a = Components.Get<NpcAnimator>();
if (!a.IsValid())
return;
Path ??= new();
UpdateActivity(a);
if (Path.Count == 0)
a.Owner.SnapToGrid();
var root = a.Owner.Renderer.GetAttachment("root").Value.Position;
root.z = 0;
root = WorldTransform.PointToWorld(WorldTransform.PointToLocal(root).WithY(0f));
if (Grid.Snap(root).Distance(root) < 5)
root = Grid.Snap(root);
//DebugOverlay.Sphere(new(root, 2), Color.Green, 0.5f);
EntityRoot = root;
if (Path.Count == 0)
return;
if (root.AlmostEqual(Path[0]))
Path.RemoveAt(0);
}
private void UpdateActivity(NpcAnimator a) {
if (!DontUpdateActivity && !a.ActivitySet.NonInteruptActivities.Contains(a.Activity)) {
a.Activity = "walk";
if (Run)
a.Activity = "jog";
if (Path.Count == 0) {
a.Activity = "idle";
return;
}
if (Path[0].AlmostEqual(WorldPosition))
Path.RemoveAt(0);
if (Path.Count == 0) {
a.Activity = "idle";
return;
}
}
if (Path.Count == 0)
return;
var newrot = Rotation.LookAt(Vector3.Direction(EntityRoot, Path[0]), Vector3.Up);
if (newrot != WorldRotation) {
WorldPosition = WorldPosition.RotateAround(EntityRoot, Rotation.Difference(WorldRotation, newrot));
WorldRotation = newrot;
a.Owner.SnapToGrid();
}
}
}