Skip to content

Commit

Permalink
Generate random path for enemy fairies
Browse files Browse the repository at this point in the history
  • Loading branch information
Helco committed Nov 3, 2024
1 parent f00e1a5 commit bf8f534
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 3 deletions.
4 changes: 3 additions & 1 deletion zzre.core/PooledList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,7 @@ public ref T Add()
public readonly ArraySegment<T>.Enumerator GetEnumerator() =>
new ArraySegment<T>(array, 0, count).GetEnumerator();
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

public static implicit operator ReadOnlySpan<T>(in PooledList<T> list) => list.Span;
}
33 changes: 33 additions & 0 deletions zzre.core/math/NumericsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,39 @@ public static T NextOf<T>(this Random random, IReadOnlyList<T> list) => !list.An
public static T NextOf<T>(this Random random) where T : struct, Enum =>
random.NextOf(Enum.GetValues<T>());

public static bool IsSorted<T>(this ReadOnlySpan<T> list)
where T : struct, IComparisonOperators<T, T, bool>
{
for (int i = 1; i < list.Length; i++)
{
if (list[i - 1] > list[i])
return false;
}
return true;
}

public static T NextOf<T>(this Random random, ReadOnlySpan<T> from, ReadOnlySpan<T> except)
where T : struct, IComparable<T>, IEquatable<T>, IComparisonOperators<T, T, bool>
{
// Used in the path finder the original code would only try 20 times and without sorting check
int index;
if (except.Length > 8 && except.IsSorted())
{
do
{
index = random.Next(from.Length);
} while (except.BinarySearch(from[index]) >= 0);
}
else
{
do
{
index = random.Next(from.Length);
} while (except.Contains(from[index]));
}
return from[index];
}

public static bool IsFinite(this Vector2 v) =>
float.IsFinite(v.X) && float.IsFinite(v.Y);

Expand Down
5 changes: 4 additions & 1 deletion zzre/game/DuelGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public DuelGame(ITagContainer diContainer, messages.StartDuel message) : base(di
new systems.effect.Sound(this),

// Fairies
new systems.AIPath(this),
new systems.FairyPhysics(this),
new systems.FairyAnimation(this),
new systems.FairyActivation(this),
Expand Down Expand Up @@ -90,7 +91,9 @@ public DuelGame(ITagContainer diContainer, messages.StartDuel message) : base(di
new systems.ModelRenderer(this, components.RenderOrder.LateSolid),
new systems.ModelRenderer(this, components.RenderOrder.LateAdditive),
new systems.effect.EffectRenderer(this, components.RenderOrder.LateEffect),
new systems.effect.EffectModelRenderer(this, components.RenderOrder.LateEffect));
new systems.effect.EffectModelRenderer(this, components.RenderOrder.LateEffect),

new systems.DebugAIPath(this));

LoadScene($"sd_{message.SceneId:D4}");
camera.Location.LocalPosition = -ecsWorld.Get<rendering.WorldMesh>().Origin;
Expand Down
42 changes: 42 additions & 0 deletions zzre/game/PathFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@

namespace zzre.game;

public enum WaypointEdgeKind
{
None,
Walkable,
Jumpable
}

public class PathFinder : IDisposable
{
public const uint InvalidId = uint.MaxValue;
Expand Down Expand Up @@ -142,6 +149,9 @@ public uint FurthestId<TFilter>(Vector3 position, TFilter filter)
return bestId;
}

public bool IsTraversable(uint waypointId) =>
IsTraversable(wpSystem.Waypoints[idToIndex[waypointId]]);

public static bool IsTraversable(in Waypoint waypoint) =>
waypoint.Group == InvalidId || waypoint.WalkableIds.Length > 0 || waypoint.JumpableIds.Length > 0;

Expand Down Expand Up @@ -180,4 +190,36 @@ private readonly struct NearestInvisibleFilter(ArraySegment<bool> isVisible) : I
public bool IsValid(in Waypoint waypoint, int index) =>
!isVisible[index] && IsTraversable(waypoint);
}

public uint TryRandomNextTraversable(uint fromId, out WaypointEdgeKind edgeKind) =>
TryRandomNextTraversable(fromId, ReadOnlySpan<uint>.Empty, out edgeKind);

Check failure on line 195 in zzre/game/PathFinder.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest) / Build

Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0301)

public uint TryRandomNextTraversable(uint fromId, ReadOnlySpan<uint> except, out WaypointEdgeKind edgeKind)
{
// TODO: Investigate weird waypoint modification from random next in original code
// The original engine would override the walkable list with the jumpable ones if it could not find
// any walkable next waypoint. This would not happen in the original code... right?!

// Also with the validation command not done yet I just suspect that there are no directed edges so
// this should degrade cleanly to: any random walkable except - otherwise any random jumpable
// which oculd be implemented a bit more efficiently and cleaner.

ref readonly var from = ref wpSystem.Waypoints[idToIndex[fromId]];
var toId = random.NextOf(from.WalkableIds, except);
if (IsTraversable(toId))
{
edgeKind = WaypointEdgeKind.Walkable;
return toId;
}

toId = random.NextOf(from.JumpableIds, except);
if (IsTraversable(toId))
{
edgeKind = WaypointEdgeKind.Jumpable;
return toId;
}

edgeKind = WaypointEdgeKind.None;
return InvalidId;
}
}
2 changes: 1 addition & 1 deletion zzre/game/components/ai/AIPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace zzre.game.components;

public struct AIPath
{
public PooledList<(uint Id, Vector3 Position)> WaypointIndices;
public PooledList<uint> WaypointIndices;
}

/*
Expand Down
28 changes: 28 additions & 0 deletions zzre/game/systems/duel/AIPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@ public sealed partial class AIPath : ISystem<float>

private readonly DefaultEcs.World ecsWorld;
private readonly IDisposable componentRemovedSubscription;
private readonly IDisposable sceneLoadedSubscription;
private readonly IDisposable generateMessageSubscription;
private PathFinder pathFinder = null!;
public bool IsEnabled { get; set; }

public AIPath(ITagContainer diContainer)
{
ecsWorld = diContainer.GetTag<DefaultEcs.World>();
ecsWorld.SetMaxCapacity<PathFinder>(1);
componentRemovedSubscription = ecsWorld.SubscribeEntityComponentRemoved<components.AIPath>(HandleComponentRemoved);
sceneLoadedSubscription = ecsWorld.Subscribe<messages.SceneLoaded>(HandleSceneLoaded);
generateMessageSubscription = ecsWorld.Subscribe<messages.GenerateAIPath>(HandleGenerateAIPath);
}

public void Dispose()
{
componentRemovedSubscription.Dispose();
sceneLoadedSubscription.Dispose();
generateMessageSubscription.Dispose();
}

Expand All @@ -34,6 +39,12 @@ private void HandleComponentRemoved(in DefaultEcs.Entity entity, in components.A
value.WaypointIndices.Dispose();
}

private void HandleSceneLoaded(in messages.SceneLoaded msg)
{
pathFinder = new PathFinder(msg.Scene.waypointSystem, ecsWorld.Get<WorldCollider>());
ecsWorld.Set(pathFinder);
}

private void HandleGenerateAIPath(in messages.GenerateAIPath message)
{
var optPath = message.ForEntity.TryGet<components.AIPath>();
Expand All @@ -48,7 +59,24 @@ private void HandleGenerateAIPath(in messages.GenerateAIPath message)
ref var path = ref optPath.Value;
path.WaypointIndices.Clear();

uint nearestId = message.CurrentWaypointId;
if (message.CurrentWaypointId == PathFinder.InvalidId)
{
var currentPosition = message.CurrentPosition ?? message.ForEntity.Get<Location>().GlobalPosition;
nearestId = pathFinder.NearestTraversableId(currentPosition);
}
if (nearestId == PathFinder.InvalidId)
return;

path.WaypointIndices.Add(nearestId);
var currentId = nearestId;
while (path.WaypointIndices.Count < 4)
{
currentId = pathFinder.TryRandomNextTraversable(currentId, path.WaypointIndices, out _);
if (currentId == PathFinder.InvalidId)
return;
path.WaypointIndices.Add(currentId);
}
}

public void Update(float _)
Expand Down
49 changes: 49 additions & 0 deletions zzre/game/systems/duel/DebugAIPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Numerics;
using DefaultEcs.System;
using Veldrid;
using zzio;
using zzre.materials;
using zzre.rendering;

namespace zzre.game.systems;

public sealed partial class DebugAIPath : AEntitySetSystem<CommandList>
{
private readonly DebugLineRenderer lineRenderer;

public DebugAIPath(ITagContainer diContainer) : base(diContainer.GetTag<DefaultEcs.World>(), CreateEntityContainer, useBuffer: false)
{
lineRenderer = new(diContainer);
lineRenderer.Material.LinkTransformsTo(diContainer.GetTag<Camera>());
lineRenderer.Material.World.Ref = Matrix4x4.Identity;
}

protected override void PreUpdate(CommandList cl)
{
base.PreUpdate(cl);
lineRenderer.Clear();
}

[Update]
private void Update(in components.AIPath aiPath)
{
var pathFinder = World.Get<PathFinder>();
foreach (var index in aiPath.WaypointIndices)
{
lineRenderer.AddDiamondSphere(new(pathFinder[index], 0.1f), IColor.White);
}

for (int i = 1; i < aiPath.WaypointIndices.Count; i++)
{
lineRenderer.Add(IColor.White,
pathFinder[aiPath.WaypointIndices[i - 1]],
pathFinder[aiPath.WaypointIndices[i]]);
}
}

protected override void PostUpdate(CommandList cl)
{
base.PostUpdate(cl);
lineRenderer.Render(cl);
}
}
2 changes: 2 additions & 0 deletions zzre/game/systems/fairy/FairyActivation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ private void HandleSwitchFairy(in messages.SwitchFairy fairy)
participant.ActiveSlot = nextSlotI;
var nextFairy = participant.ActiveFairy;
nextFairy.Enable();
if (fairy.Participant != ecsWorld.Get<components.PlayerEntity>().Entity)
ecsWorld.Publish(new messages.GenerateAIPath(nextFairy));
}

private (Vector3 pos, Vector3 dir) FindFarthestStartPoint()
Expand Down

0 comments on commit bf8f534

Please sign in to comment.