Skip to content

Instantly share code, notes, and snippets.

@Hexlord
Last active October 31, 2022 12:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Hexlord/cf522316cbca6c92236d7a392eaf5439 to your computer and use it in GitHub Desktop.
Save Hexlord/cf522316cbca6c92236d7a392eaf5439 to your computer and use it in GitHub Desktop.
#include "Collision.h"
#include "Modules/DebugPrimitives/DebugPrimitives.h"
#include "Modules/Environment/Environment.h"
#include "Modules/Health/Health.h"
#include "Modules/Movement/Movement.h"
#include "Modules/Primitives/PrimitivesTypes.h"
#include "Modules/Transform/Transform.h"
namespace SE {
struct FColliderCircle {
fvec2 Position;
float Radius;
};
struct FColliderRect {
TStaticArray<fvec2, 4> Polygon;
};
using FCollider = TVariant<FColliderCircle, FColliderRect>;
struct FSpatialHashCollider {
FCollider Collider;
entity_id EntityId;
// inline bool operator==(const FSpatialHashCollider &Other) const {
// return EntityId == Other.EntityId;
// }
// inline bool operator!=(const FSpatialHashCollider &Other) const {
// return !operator==(Other);
// }
inline operator uint64() const {
return EntityId;
}
};
struct FSpatialHashColliderDynamic {
FCollider Collider;
fvec2 Velocity;
entity_id EntityId;
// inline bool operator==(const FSpatialHashColliderDynamic &Other) const {
// return EntityId == Other.EntityId;
// }
// inline bool operator!=(const FSpatialHashColliderDynamic &Other) const {
// return !operator==(Other);
// }
inline operator uint64() const {
return EntityId;
}
};
struct FSpatialHashCell {
THeapArray<FSpatialHashCollider> StaticCollision;
THeapArray<FSpatialHashColliderDynamic> DynamicCollision;
THeapArray<FSpatialHashCollider> TriggerCollision;
fvec2 CellPosition;
};
struct FSpatialHashQueryResult {
TFrameArray<const FSpatialHashCollider *> StaticCollision;
TFrameArray<const FSpatialHashColliderDynamic *> DynamicCollision;
TFrameArray<const FSpatialHashCollider *> TriggerCollision;
};
struct FSpatialHashQueryFilter {
bool AllowStaticCollision = true;
bool AllowDynamicCollision = true;
bool AllowTriggerCollision = true;
TFrameArray<entity_id> RequiredComponents;
};
//
struct CDisplacement {
fvec2 Displacement = {0.0f, 0.0f};
};
enum class EColliderCacheCollisionCategory {
None,
Static,
Dynamic,
Trigger
};
struct Reflection(NoSerialize, NoCopy) CColliderCache {
THeapArray<ivec2> Cells;
EColliderCacheCollisionCategory CollisionCategory = EColliderCacheCollisionCategory::None;
};
// Compound collision is just multiple children with CCircleCollision and CCollisionRedirector to their parent
// Use circle at CWorldTransform's position as collider.
struct CColliderCircle {
float Radius = 50.0f;
};
struct CColliderRect {
fvec2 Size = {100.0f, 100.0f};
};
struct CColliderRectFromSize {
};
struct CStaticCollision {
};
struct CDynamicCollision {
float Mass = 0.0f;
};
struct CRVOAvoidance {
fvec2 DesiredVelocity = {0.0f, 0.0f};
};
namespace {
struct FORCAObstaclePoint {
fvec2 Position;
fvec2 Direction;
bool Convex;
};
struct FCollisionStorage {
THashSet<uint64, THasherBypass> ProcessedEntities;
THashSet<FSpatialHashCollider, THasherBypass, uint64> PotentialStaticCollisions;
THashSet<FSpatialHashColliderDynamic, THasherBypass, uint64> PotentialDynamicCollisions;
THeapArray<FORCALine> ORCALines;
byte Padding0[Meta::MaxCacheLineSize];
};
byte Padding0[Meta::MaxCacheLineSize];
TStaticArray<FCollisionStorage, SE::Impl::MaxThreads> AllCollisionStorages;
constexpr int32 CellSize = 60;
constexpr float CellSizeFloat = (float)CellSize;
constexpr float DebugVisualizationRange = 400.0f;
// Careful with changing this, too high or too low makes behavior strange.
constexpr float RVOTimeHorizon = 10.0f;
constexpr float RVOTimeHorizonInverted = 1.0f / RVOTimeHorizon;
// Careful with changing this, too high or too low makes behavior strange.
constexpr float RVOTimeHorizonObstacle = 10.0f;
constexpr float RVOTimeHorizonObstacleInverted = 1.0f / RVOTimeHorizonObstacle;
// This is not a part of the algorithm, but it makes dead-end scenarios like circles resolve much-much faster.
constexpr float RVORadiusCoef = 1.1f;
constexpr float RVOAvoidanceRadiusMultiplier = 15.0f;
constexpr float RVOAvoidanceObstacleRadiusMultiplier = 1.5f;
constexpr float RVOEpsilon = 0.0001f;
constexpr float DisplacementPower = 0.7f;
constexpr float DisplacementConst = 5.0f;
const float DisplacementConstReversePowered = Math::Pow(DisplacementConst, 1.0f / DisplacementPower);
THeapArray<ivec2> CellsRemain;
THeapArray<ivec2> CellsAdded;
THeapArray<ivec2> CellsRemoved;
// 50% load factor for better performance.
THashMap<uint64, FSpatialHashCell, THasherBypass, uint64, 5u> SpatialHash;
struct FSpatialProjectionResult {
ivec2 Start;
ivec2 End;
};
void GenerateObstacleORCALines(THeapArray<FORCALine> &ORCALines, fvec2 Position, float Radius, fvec2 Velocity, TArrayView<fvec2> Polygon);
void GenerateObstacleORCALine(THeapArray<FORCALine> &ORCALines, fvec2 Position, float Radius, fvec2 Velocity, TArrayView<FORCAObstaclePoint> Points);
void GenerateEntityORCALine(
THeapArray<FORCALine> &ORCALines, fvec2 Delta, float Radius, fvec2 Velocity, const FSpatialHashColliderDynamic &ColliderDynamic);
uint64 AssembleKey(ivec2 Cell);
uint64 AssembleKey(fvec2 Position);
FSpatialProjectionResult Project(const FCollider &Collider);
void ProcessCollider(
entity Entity, const FCollider &Collider, const CStaticCollision *StaticCollision, const CDynamicCollision *DynamicCollision,
const CWorldTransform &WorldTransform, const CWorldTransformPreviousPosition &WorldTransformPreviousPosition,
const CMovementVelocity *MovementVelocity, CColliderCache &ColliderCache, const CDebugRenderSettings &DebugRenderSettings,
const CCamera2 &Camera);
bool DoCollide(const FCollider &ColliderA, const FCollider &ColliderB);
} // namespace
MCollisionComponents::MCollisionComponents(ecs &ECS) {
Register<CDisplacement>(ECS);
Register<CStaticCollision>(ECS);
Register<CDynamicCollision>(ECS);
RegisterWith<CDynamicCollision, CDisplacement>(ECS);
Register<CColliderCache>(ECS);
Register<CColliderCircle>(ECS);
RegisterWith<CColliderCircle, CColliderCache>(ECS);
RegisterWith<CColliderCircle, CWorldTransformPreviousPosition>(ECS);
Register<CColliderRect>(ECS);
RegisterWith<CColliderRect, CColliderCache>(ECS);
RegisterWith<CColliderRect, CWorldTransformPreviousPosition>(ECS);
Register<CColliderRectFromSize>(ECS);
RegisterWith<CColliderRectFromSize, CColliderRect>(ECS);
Register<CRVOAvoidance>(ECS);
SpatialHash = {};
}
MCollisionColliderSystemsMT::MCollisionColliderSystemsMT(ecs &ECS) {
MultiThreadedSystem<
const CColliderRectFromSize, CColliderRect, const CWorldTransform, const CUINode *, const CTextSize *, const CShapeSize *,
const CScaleBySpriteFrameSize *, const CSpriteFrameSize *, const CScaleByTextureSize *, const CTextureSize *, const CScaleByCustomSize *,
const CCustomSize *>(
ECS, "Collision|Collider",
[](entity Entity, CColliderRect &ColliderRect, const CWorldTransform &WorldTransform, const CUINode *Node, const CTextSize *TextSize,
const CShapeSize *ShapeSize, const CScaleBySpriteFrameSize *ScaleBySpriteFrameSize, const CSpriteFrameSize *SpriteFrameSize,
const CScaleByTextureSize *ScaleByTextureSize, const CTextureSize *TextureSize, const CScaleByCustomSize *ScaleByCustomSize,
const CCustomSize *CustomSize) {
var ScaleAndSize = Transform::ResolveScaleAndSize(
&WorldTransform, Node, TextSize, ShapeSize, ScaleBySpriteFrameSize, SpriteFrameSize, ScaleByTextureSize, TextureSize,
ScaleByCustomSize, CustomSize);
ColliderRect.Size = ScaleAndSize.Size;
});
}
void MCollisionGatherSystemsST(ecs &ECS) {
QueryIteration<
const CColliderCircle, const CStaticCollision *, const CDynamicCollision *, const CWorldTransform, const CWorldTransformPreviousPosition,
const CMovementVelocity *, CColliderCache, const CDebugRenderSettings &, const CCamera2 &>(
ECS, "Collision|Gather",
[](entity Entity, const CColliderCircle &CircleCollision, const CStaticCollision *StaticCollision, const CDynamicCollision *DynamicCollision,
const CWorldTransform &WorldTransform, const CWorldTransformPreviousPosition &WorldTransformPreviousPosition,
const CMovementVelocity *MovementVelocity, CColliderCache &ColliderCache, const CDebugRenderSettings &DebugRenderSettings,
const CCamera2 &Camera) {
FColliderCircle Circle = {WorldTransform.Position, CircleCollision.Radius};
ProcessCollider(
Entity, Circle, StaticCollision, DynamicCollision, WorldTransform, WorldTransformPreviousPosition, MovementVelocity, ColliderCache,
DebugRenderSettings, Camera);
});
QueryIteration<
const CColliderRect, const CStaticCollision *, const CDynamicCollision *, const CWorldTransform, const CWorldTransformPreviousPosition,
const CMovementVelocity *, CColliderCache, const CDebugRenderSettings &, const CCamera2 &>(
ECS, "Collision|Gather",
[](entity Entity, const CColliderRect &ColliderRect, const CStaticCollision *StaticCollision, const CDynamicCollision *DynamicCollision,
const CWorldTransform &WorldTransform, const CWorldTransformPreviousPosition &WorldTransformPreviousPosition,
const CMovementVelocity *MovementVelocity, CColliderCache &ColliderCache, const CDebugRenderSettings &DebugRenderSettings,
const CCamera2 &Camera) {
FColliderRect Rect;
var HalfSize = ColliderRect.Size * 0.5f;
Rect.Polygon[0] = WorldTransform.Position + Math::Rotate(fvec2{-HalfSize.X, +HalfSize.Y}, WorldTransform.SinCos);
Rect.Polygon[1] = WorldTransform.Position + Math::Rotate(fvec2{+HalfSize.X, +HalfSize.Y}, WorldTransform.SinCos);
Rect.Polygon[2] = WorldTransform.Position + Math::Rotate(fvec2{+HalfSize.X, -HalfSize.Y}, WorldTransform.SinCos);
Rect.Polygon[3] = WorldTransform.Position + Math::Rotate(fvec2{-HalfSize.X, -HalfSize.Y}, WorldTransform.SinCos);
ProcessCollider(
Entity, Rect, StaticCollision, DynamicCollision, WorldTransform, WorldTransformPreviousPosition, MovementVelocity, ColliderCache,
DebugRenderSettings, Camera);
});
Profiled("Collision|Debug", [&]() {
ref DebugRenderSettings = Ref<CDebugRenderSettings>(ECS);
if(DebugRenderSettings.DebugRender) {
ref Camera2 = Ref<const CCamera2>(ECS);
var Projection = Project(FColliderCircle{Camera2.Position, DebugVisualizationRange});
for(var X = Projection.Start.X; X <= Projection.End.X; ++X) {
for(var Y = Projection.Start.Y; Y <= Projection.End.Y; ++Y) {
var Key = AssembleKey(ivec2{X, Y});
var StaticCount = 0u;
var DynamicCount = 0u;
var NoCount = 0u;
if(var Existing = SpatialHash.Find(Key)) {
StaticCount = Existing->StaticCollision.GetSize();
DynamicCount = Existing->DynamicCollision.GetSize();
NoCount = Existing->TriggerCollision.GetSize();
}
var RedStrength = Math::Sqrt(0.5f - 1.0f / (float)(DynamicCount + 2u));
var GreenStrength = Math::Sqrt(0.5f - 1.0f / (float)(StaticCount + 2u));
var BlueStrength = Math::Sqrt(0.5f - 1.0f / (float)(NoCount + 2u));
var Color = fcolor4{
RedStrength, GreenStrength, BlueStrength,
Math::Sqrt(RedStrength * 0.33f + GreenStrength * 0.33f + BlueStrength * 0.33f) * 1.0f};
if(!Math::IsAlmostZero(Color.A)) {
Debug::DebugFilledRect(
ECS, fvec2{((float)X + 0.5f) * CellSizeFloat, ((float)Y + 0.5f) * CellSizeFloat}, 0.0f,
fvec2{CellSizeFloat, CellSizeFloat} * 0.95f, Color, 0.0f, false);
}
}
}
}
});
}
void MCollisionDestructSystemsST(ecs &ECS) {
QueryIteration<const CStaticCollision *, const CDynamicCollision *, CColliderCache, const CBeingDestructed>(
ECS, "Collision|Gather",
[](entity Entity, const CStaticCollision *StaticCollision, const CDynamicCollision *DynamicCollision, CColliderCache &ColliderCache) {
var EntityId = Entity.id();
for(var Cell: ColliderCache.Cells) {
var Key = AssembleKey(Cell);
var Bucket = SpatialHash.ComputeBucket(Key);
FSpatialHashCell *SpatialHashCell = &SpatialHash.Get(Key, Bucket);
if(StaticCollision) {
Array::RemoveSwap(SpatialHashCell->StaticCollision, [EntityId](const FSpatialHashCollider &Collider) {
return Collider.EntityId == EntityId;
});
} else if(DynamicCollision) {
Array::RemoveSwap(SpatialHashCell->DynamicCollision, [EntityId](const FSpatialHashColliderDynamic &Collider) {
return Collider.EntityId == EntityId;
});
} else {
Array::RemoveSwap(SpatialHashCell->TriggerCollision, [EntityId](const FSpatialHashCollider &Collider) {
return Collider.EntityId == EntityId;
});
}
}
});
}
MCollisionDisplacementResolveSystemsMT::MCollisionDisplacementResolveSystemsMT(ecs &ECS) {
SkipDuringPause([&]() {
MultiThreadedSystem<CLocalTransform, CWorldTransform, CMovementVelocity *, CDisplacement>(
ECS, "Collision|DisplacementResolve",
[](entity Entity, CLocalTransform &LocalTransform, CWorldTransform &WorldTransform, CMovementVelocity *MovementVelocity,
CDisplacement &Displacement) {
// var SlowDownFactor = Math::Min(Math::Length(Displacement.Displacement) / Math::Length(MovementVelocity.Velocity), 1.0f);
// MovementVelocity.Velocity *= 1.0f - SlowDownFactor;
if(MovementVelocity) {
MovementVelocity->Velocity += Displacement.Displacement * 0.25f;
}
// Debugf("Displacing %s by %.3f %.3f", *EntityName(Entity), Displacement.Displacement.X, Displacement.Displacement.Y);
WorldTransform.Position += Displacement.Displacement * 0.5f;
Check(!Math::IsNan(WorldTransform.Position));
LocalTransform.SetOverridenByWorldTransform();
Displacement.Displacement = {0.0f, 0.0f};
});
});
}
MCollisionUpdateSystemsMT::MCollisionUpdateSystemsMT(ecs &ECS) {
SkipDuringPause([&]() {
MultiThreadedSystem<
const CColliderCircle, const CDynamicCollision, const CWorldTransform, CDisplacement, CMovementVelocity *, const CMovementMaximumSpeed *,
CMovementPreferredVelocity *, const CAIMovementToEntity *, CRVOAvoidance *, const CDebugRenderSettings &>(
ECS, "Collision|Update&Debug",
[](entity Entity, const CColliderCircle &ColliderCircle, const CWorldTransform &WorldTransform, CDisplacement &Displacement,
CMovementVelocity *Velocity, const CMovementMaximumSpeed *MaximumSpeed, CMovementPreferredVelocity *PreferredVelocity,
const CAIMovementToEntity *AIMovementToEntity, CRVOAvoidance *RVOAvoidance, const CDebugRenderSettings &DebugRenderSettings) {
var EntityId = Entity.id();
// Debugf("%s", *EntityName(Entity));
var ECS = Entity.world();
ref Storage = AllCollisionStorages[Impl::ThisThreadIndex];
Storage.PotentialStaticCollisions.Clear();
Storage.PotentialDynamicCollisions.Clear();
Displacement.Displacement = {0.0f, 0.0f};
var ProjectionRadius = ColliderCircle.Radius;
if(RVOAvoidance) {
ProjectionRadius = Math::Max(ProjectionRadius, ColliderCircle.Radius * RVOAvoidanceRadiusMultiplier);
}
var Projection = Project(FColliderCircle{WorldTransform.Position, ProjectionRadius});
for(var X = Projection.Start.X; X <= Projection.End.X; ++X) {
for(var Y = Projection.Start.Y; Y <= Projection.End.Y; ++Y) {
var Key = AssembleKey(ivec2{X, Y});
if(var Existing = SpatialHash.Find(Key)) {
for(ref ColliderStatic: Existing->StaticCollision) {
if(ColliderStatic.EntityId != EntityId) {
Storage.PotentialStaticCollisions.TryAdd(ColliderStatic);
}
}
}
if(var Existing = SpatialHash.Find(Key)) {
for(ref ColliderDynamic: Existing->DynamicCollision) {
if(ColliderDynamic.EntityId != EntityId) {
Storage.PotentialDynamicCollisions.TryAdd(ColliderDynamic);
}
}
}
}
}
if(RVOAvoidance) {
Storage.ORCALines.Clear();
}
for(ref PotentialStaticCollision: Storage.PotentialStaticCollisions) {
FColliderCircle Collider = PotentialStaticCollision.Collider;
var ColliderPosition = Collider.Position;
var Delta = WorldTransform.Position - ColliderPosition;
var Distance = Math::Length(Delta);
var DisplacementAmount = (ColliderCircle.Radius + Collider.Radius) - Distance;
var Collision = DisplacementAmount > 0.0f;
if(Collision) {
var Direction = Math::SafeDivision(Delta, Distance, fvec2{1.0f, 0.0f});
Displacement.Displacement +=
Direction * (Math::Pow(DisplacementAmount + DisplacementConstReversePowered, DisplacementPower) - DisplacementConst);
// Displacement.Displacement += Direction * DisplacementAmount;
}
if(RVOAvoidance) {
var Radius = Collider.Radius * RVOAvoidanceObstacleRadiusMultiplier;
var Polygon = Geom::CreateCircle<FFrameAllocator>(ColliderPosition, Radius, 15.0f);
GenerateObstacleORCALines(
Storage.ORCALines, WorldTransform.Position, ColliderCircle.Radius, Velocity->Velocity, View(Polygon));
}
}
var ObstacleCount = Storage.ORCALines.GetSize();
for(ref PotentialDynamicCollision: Storage.PotentialDynamicCollisions) {
// var PotentialDynamicCollisionEntity = ECS.entity(PotentialDynamicCollision.EntityId);
FColliderCircle Collider = PotentialDynamicCollision.Collider;
var ColliderPosition = Collider.Position;
var Delta = WorldTransform.Position - ColliderPosition;
var Distance = Math::Length(Delta);
var DisplacementAmount = (ColliderCircle.Radius + Collider.Radius) - Distance;
var Collision = DisplacementAmount > 0.0f;
if(Collision) {
var Direction = Math::SafeDivision(Delta, Distance, fvec2{1.0f, 0.0f});
Displacement.Displacement +=
Direction * (Math::Pow(DisplacementAmount + DisplacementConstReversePowered, DisplacementPower) - DisplacementConst);
Check(!Math::IsNan(Displacement.Displacement));
}
if(RVOAvoidance) {
if(!(AIMovementToEntity && AIMovementToEntity->Target.entity().id() == PotentialDynamicCollision.EntityId)) {
var CollisionEntity = ECS.entity(PotentialDynamicCollision.EntityId);
// GenerateEntityORCALine(Delta, ColliderCircle.Radius, Velocity->Velocity, PotentialDynamicCollision);
// Only expect from living entities to move.
if(CollisionEntity && Alive(CollisionEntity) && Has<CRVOAvoidance>(CollisionEntity)) {
GenerateEntityORCALine(
Storage.ORCALines, Delta, ColliderCircle.Radius, Velocity->Velocity, PotentialDynamicCollision);
} else {
// Interpret non-rvo agents as static obstacles.
var Radius = Collider.Radius * RVOAvoidanceObstacleRadiusMultiplier;
var Polygon = Geom::CreateCircle<FFrameAllocator>(ColliderPosition, Radius, 15.0f);
GenerateObstacleORCALines(
Storage.ORCALines, WorldTransform.Position, ColliderCircle.Radius, Velocity->Velocity, View(Polygon));
}
}
}
}
if(RVOAvoidance) {
fvec2 NewVelocity;
var LineFail =
RVO::LinearProgram2(View(Storage.ORCALines), MaximumSpeed->Speed, PreferredVelocity->PreferredVelocity, false, NewVelocity);
Check(!Math::IsNan(NewVelocity));
if(LineFail < Storage.ORCALines.GetSize()) {
RVO::LinearProgram3(View(Storage.ORCALines), ObstacleCount, LineFail, MaximumSpeed->Speed, NewVelocity);
}
RVOAvoidance->DesiredVelocity = NewVelocity;
Check(!Math::IsNan(NewVelocity));
}
});
});
}
namespace Collision {
// Gather is single-threaded, so we don't need locks.
FSpatialHashQueryResult Query(ecs &ECS, const FCollider &InCollider, const FSpatialHashQueryFilter &Filter) {
ref Storage = AllCollisionStorages[Impl::ThisThreadIndex];
Storage.ProcessedEntities.Clear();
FSpatialHashQueryResult Result;
var Projection = Project(InCollider);
var ConsiderCollider = [&](const FSpatialHashCollider &Collider) {
var Bucket = Storage.ProcessedEntities.ComputeBucket(Collider.EntityId);
if(Storage.ProcessedEntities.Contains(Collider.EntityId, Bucket)) {
return false;
}
Storage.ProcessedEntities.Add(Collider.EntityId, Bucket);
var Entity = ECS.entity(Collider.EntityId);
if(!Entity) {
return false;
}
for(var Component: Filter.RequiredComponents) {
if(!Entity.has(Component)) {
return false;
}
}
if(!DoCollide(InCollider, Collider.Collider)) {
return false;
}
return true;
};
var ConsiderColliderDynamic = [&](const FSpatialHashColliderDynamic &Collider) {
var Bucket = Storage.ProcessedEntities.ComputeBucket(Collider.EntityId);
if(Storage.ProcessedEntities.Contains(Collider.EntityId, Bucket)) {
return false;
}
Storage.ProcessedEntities.Add(Collider.EntityId, Bucket);
var Entity = ECS.entity(Collider.EntityId);
if(!Entity) {
return false;
}
for(var Component: Filter.RequiredComponents) {
if(!Entity.has(Component)) {
return false;
}
}
if(!DoCollide(InCollider, Collider.Collider)) {
return false;
}
return true;
};
for(var X = Projection.Start.X; X <= Projection.End.X; ++X) {
for(var Y = Projection.Start.Y; Y <= Projection.End.Y; ++Y) {
var Key = AssembleKey(ivec2{X, Y});
if(var Existing = SpatialHash.Find(Key)) {
if(Filter.AllowStaticCollision) {
for(ref StaticCollision: Existing->StaticCollision) {
if(ConsiderCollider(StaticCollision)) {
Result.StaticCollision.Add(&StaticCollision);
}
}
}
if(Filter.AllowDynamicCollision) {
for(ref DynamicCollision: Existing->DynamicCollision) {
if(ConsiderColliderDynamic(DynamicCollision)) {
Result.DynamicCollision.Add(&DynamicCollision);
}
}
}
if(Filter.AllowTriggerCollision) {
for(ref TriggerCollision: Existing->TriggerCollision) {
if(ConsiderCollider(TriggerCollision)) {
Result.TriggerCollision.Add(&TriggerCollision);
}
}
}
}
}
}
return Result;
}
} // namespace Collision
namespace {
bool DoCollide(const FCollider &ColliderA, const FCollider &ColliderB) {
if(const FColliderCircle *CircleA = ColliderA) {
if(const FColliderCircle *CircleB = ColliderB) {
return Math::DistanceSquared(CircleA->Position, CircleB->Position) <= Math::Square(CircleA->Radius + CircleB->Radius);
}
if(const FColliderRect *RectB = ColliderB) {
for(var Index = 0u; Index < 3u; ++Index) {
if(Math::OverlapsSegmentToCircle(RectB->Polygon[Index + 0u], RectB->Polygon[Index + 1u], CircleA->Position, CircleA->Radius)) {
return true;
}
}
if(Math::OverlapsSegmentToCircle(RectB->Polygon[3u], RectB->Polygon[0u], CircleA->Position, CircleA->Radius)) {
return true;
}
if(Math::IsInsideConvexPolygon(CircleA->Position, View(RectB->Polygon))) {
return true;
}
}
}
if(const FColliderRect *RectA = ColliderA) {
if(const FColliderCircle *CircleB = ColliderB) {
for(var Index = 0u; Index < 3u; ++Index) {
if(Math::OverlapsSegmentToCircle(RectA->Polygon[Index + 0u], RectA->Polygon[Index + 1u], CircleB->Position, CircleB->Radius)) {
return true;
}
}
if(Math::OverlapsSegmentToCircle(RectA->Polygon[3u], RectA->Polygon[0u], CircleB->Position, CircleB->Radius)) {
return true;
}
if(Math::IsInsideConvexPolygon(CircleB->Position, View(RectA->Polygon))) {
return true;
}
}
if(const FColliderRect *RectB = ColliderB) {
return Math::ContainsOrOverlapsConvex(View(RectA->Polygon), View(RectB->Polygon));
}
}
return false;
}
void GenerateObstacleORCALines(THeapArray<FORCALine> &ORCALines, fvec2 Position, float Radius, fvec2 Velocity, TArrayView<fvec2> Polygon) {
Check(Polygon.GetSize() >= 3u);
var SizeMinusOne = Polygon.GetSize() - 1u;
TFrameArray<FORCAObstaclePoint> Points;
Points.EnsureMaxSize(Polygon.GetSize());
for(var Index = 0u; Index < Polygon.GetSize(); ++Index) {
fvec2 Current = Polygon[Index];
fvec2 Next = (Index == SizeMinusOne ? Polygon.First() : Polygon[Index + 1u]);
FORCAObstaclePoint Point;
Point.Position = Current;
Point.Direction = Math::Normalized(Next - Current);
Point.Convex = true;
Points.Add(Point);
}
for(var Index = 0u; Index < Points.GetSize(); ++Index) {
TStaticArray<FORCAObstaclePoint, 3> InnerPoints;
InnerPoints[0] = Index == 0u ? Points.Last() : Points[Index - 1u];
InnerPoints[1] = Points[Index];
InnerPoints[2] = Index == SizeMinusOne ? Points.First() : Points[Index + 1u];
GenerateObstacleORCALine(ORCALines, Position, Radius, Velocity, View(InnerPoints));
}
}
void GenerateObstacleORCALine(THeapArray<FORCALine> &ORCALines, fvec2 Position, float Radius, fvec2 Velocity, TArrayView<FORCAObstaclePoint> Points) {
var Obstacle1 = Points[1].Position;
var Obstacle2 = Points[2].Position;
var Direction0 = Points[0].Direction;
var Direction1 = Points[1].Direction;
var Direction2 = Points[2].Direction;
var Convex1 = Points[1].Convex;
var Convex2 = Points[2].Convex;
fvec2 relativePosition1 = Obstacle1 - Position;
fvec2 relativePosition2 = Obstacle2 - Position;
/* Check if velocity obstacle of obstacle is already taken care of by
* previously constructed obstacle ORCA lines. */
for(var j = 0u; j < ORCALines.GetSize(); ++j) {
if(Math::Cross(RVOTimeHorizonObstacleInverted * relativePosition1 - ORCALines[j].Position, ORCALines[j].Direction)
- RVOTimeHorizonObstacleInverted * Radius
>= -RVOEpsilon
&& Math::Cross(RVOTimeHorizonObstacleInverted * relativePosition2 - ORCALines[j].Position, ORCALines[j].Direction)
- RVOTimeHorizonObstacleInverted * Radius
>= -RVOEpsilon) {
return;
}
}
/* Not yet covered. Check for collisions. */
float distSq1 = Math::LengthSquared(relativePosition1);
float distSq2 = Math::LengthSquared(relativePosition2);
float radiusSq = Radius * Radius;
fvec2 obstacleVector = Obstacle2 - Obstacle1;
float s = (Math::Dot(-relativePosition1, obstacleVector)) / Math::LengthSquared(obstacleVector);
float distSqLine = Math::LengthSquared(-relativePosition1 - s * obstacleVector);
FORCALine line;
if(s < 0.0F && distSq1 <= radiusSq) {
/* Collision with left vertex. Ignore if non-convex. */
if(Convex1) {
line.Position = fvec2{0.0f, 0.0f};
line.Direction = Math::Normalized(fvec2{-relativePosition1.Y, relativePosition1.X});
Check(!Math::IsNan(line.Direction));
ORCALines.Add(line);
}
return;
}
if(s > 1.0F && distSq2 <= radiusSq) {
/* Collision with right vertex. Ignore if non-convex or if it will be
* taken care of by neighoring obstace */
if(Convex2 && Math::Cross(relativePosition2, Direction2) >= 0.0F) {
line.Position = fvec2{0.0f, 0.0f};
line.Direction = Math::Normalized(fvec2{-relativePosition2.Y, relativePosition2.X});
Check(!Math::IsNan(line.Direction));
ORCALines.Add(line);
}
return;
}
if(s >= 0.0F && s <= 1.0F && distSqLine <= radiusSq) {
/* Collision with obstacle segment. */
line.Position = fvec2{0.0f, 0.0f};
line.Direction = -Direction1;
Check(!Math::IsNan(line.Direction));
ORCALines.Add(line);
return;
}
/* No collision. Compute legs. When obliquely viewed, both legs can come
* from a single vertex. Legs extend cut-off line when nonconvex vertex. */
fvec2 leftLegDirection;
fvec2 rightLegDirection;
if(s < 0.0F && distSqLine <= radiusSq) {
/* Obstacle viewed obliquely so that left vertex defines velocity
* obstacle. */
if(!Convex1) {
/* Ignore obstacle. */
return;
}
Obstacle2 = Obstacle1;
float leg1 = Math::Sqrt(distSq1 - radiusSq);
leftLegDirection =
fvec2{relativePosition1.X * leg1 - relativePosition1.Y * Radius, relativePosition1.X * Radius + relativePosition1.Y * leg1} / distSq1;
rightLegDirection =
fvec2{relativePosition1.X * leg1 + relativePosition1.Y * Radius, -relativePosition1.X * Radius + relativePosition1.Y * leg1} / distSq1;
} else if(s > 1.0F && distSqLine <= radiusSq) {
/* Obstacle viewed obliquely so that right vertex defines velocity
* obstacle. */
if(!Convex2) {
/* Ignore obstacle. */
return;
}
Obstacle1 = Obstacle2;
float leg2 = Math::Sqrt(distSq2 - radiusSq);
leftLegDirection =
fvec2{relativePosition2.X * leg2 - relativePosition2.Y * Radius, relativePosition2.X * Radius + relativePosition2.Y * leg2} / distSq2;
rightLegDirection =
fvec2{relativePosition2.X * leg2 + relativePosition2.Y * Radius, -relativePosition2.X * Radius + relativePosition2.Y * leg2} / distSq2;
} else {
/* Usual situation. */
if(Convex1) {
float leg1 = Math::Sqrt(distSq1 - radiusSq);
leftLegDirection =
fvec2{relativePosition1.X * leg1 - relativePosition1.Y * Radius, relativePosition1.X * Radius + relativePosition1.Y * leg1} / distSq1;
} else {
/* Left vertex non-convex; left leg extends cut-off line. */
leftLegDirection = -Direction1;
}
if(Convex2) {
float leg2 = Math::Sqrt(distSq2 - radiusSq);
rightLegDirection =
fvec2{relativePosition2.X * leg2 + relativePosition2.Y * Radius, -relativePosition2.X * Radius + relativePosition2.Y * leg2}
/ distSq2;
} else {
/* Right vertex non-convex; right leg extends cut-off line. */
rightLegDirection = Direction1;
}
}
/* Legs can never point into neighboring edge when convex vertex, take
* cutoff-line of neighboring edge instead. If velocity projected on
* "foreign" leg, no constraint is added. */
bool isLeftLegForeign = false;
bool isRightLegForeign = false;
if(Convex1 && Math::Cross(leftLegDirection, -Direction0) >= 0.0F) {
/* Left leg points into obstacle. */
leftLegDirection = -Direction0;
isLeftLegForeign = true;
}
if(Convex2 && Math::Cross(rightLegDirection, Direction2) <= 0.0F) {
/* Right leg points into obstacle. */
rightLegDirection = Direction2;
isRightLegForeign = true;
}
/* Compute cut-off centers. */
fvec2 leftCutoff = RVOTimeHorizonObstacleInverted * (Obstacle1 - Position);
fvec2 rightCutoff = RVOTimeHorizonObstacleInverted * (Obstacle2 - Position);
fvec2 cutoffVec = rightCutoff - leftCutoff;
/* Project current velocity on velocity obstacle. */
/* Check if current velocity is projected on cutoff circles. */
float t = Obstacle1 == Obstacle2 ? 0.5F : Math::Dot((Velocity - leftCutoff), cutoffVec) / Math::LengthSquared(cutoffVec);
float tLeft = Math::Dot((Velocity - leftCutoff), leftLegDirection);
float tRight = Math::Dot((Velocity - rightCutoff), rightLegDirection);
if((t < 0.0F && tLeft < 0.0F) || (Obstacle1 == Obstacle2 && tLeft < 0.0F && tRight < 0.0F)) {
/* Project on left cut-off circle. */
fvec2 unitW = Math::Normalized(Velocity - leftCutoff);
line.Direction = fvec2{unitW.Y, -unitW.X};
Check(!Math::IsNan(line.Direction));
line.Position = leftCutoff + Radius * RVOTimeHorizonObstacleInverted * unitW;
ORCALines.Add(line);
return;
}
if(t > 1.0F && tRight < 0.0F) {
/* Project on right cut-off circle. */
fvec2 unitW = Math::Normalized(Velocity - rightCutoff);
line.Direction = fvec2{unitW.Y, -unitW.X};
Check(!Math::IsNan(line.Direction));
line.Position = rightCutoff + Radius * RVOTimeHorizonObstacleInverted * unitW;
ORCALines.Add(line);
return;
}
/* Project on left leg, right leg, or cut-off line, whichever is closest to
* velocity. */
float distSqCutoff = (t < 0.0F || t > 1.0F || Obstacle1 == Obstacle2) ? std::numeric_limits<float>::infinity()
: Math::LengthSquared(Velocity - (leftCutoff + t * cutoffVec));
float distSqLeft =
tLeft < 0.0F ? std::numeric_limits<float>::infinity() : Math::LengthSquared(Velocity - (leftCutoff + tLeft * leftLegDirection));
float distSqRight =
tRight < 0.0F ? std::numeric_limits<float>::infinity() : Math::LengthSquared(Velocity - (rightCutoff + tRight * rightLegDirection));
if(distSqCutoff <= distSqLeft && distSqCutoff <= distSqRight) {
/* Project on cut-off line. */
line.Direction = -Direction1;
line.Position = leftCutoff + Radius * RVOTimeHorizonObstacleInverted * fvec2{-line.Direction.Y, line.Direction.X};
ORCALines.Add(line);
return;
}
if(distSqLeft <= distSqRight) {
/* Project on left leg. */
if(isLeftLegForeign) {
return;
}
line.Direction = leftLegDirection;
Check(!Math::IsNan(line.Direction));
line.Position = leftCutoff + Radius * RVOTimeHorizonObstacleInverted * fvec2{-line.Direction.Y, line.Direction.X};
ORCALines.Add(line);
return;
}
/* Project on right leg. */
if(isRightLegForeign) {
return;
}
line.Direction = -rightLegDirection;
Check(!Math::IsNan(line.Direction));
line.Position = rightCutoff + Radius * RVOTimeHorizonObstacleInverted * fvec2{-line.Direction.Y, line.Direction.X};
ORCALines.Add(line);
}
void GenerateEntityORCALine(
THeapArray<FORCALine> &ORCALines, fvec2 Delta, float Radius, fvec2 Velocity, const FSpatialHashColliderDynamic &ColliderDynamic) {
fvec2 relativePosition = -Delta;
fvec2 relativeVelocity = Velocity - ColliderDynamic.Velocity;
float distSq = Math::LengthSquared(relativePosition);
FColliderCircle Collider = ColliderDynamic.Collider;
float combinedRadius = (Radius + Collider.Radius) * RVORadiusCoef;
float combinedRadiusSq = combinedRadius * combinedRadius;
FORCALine line;
fvec2 u;
if(distSq > combinedRadiusSq) {
/* No collision. */
fvec2 w = relativeVelocity - RVOTimeHorizonInverted * relativePosition;
/* Vector from cutoff center to relative velocity. */
float wLengthSq = Math::LengthSquared(w);
float dotProduct1 = Math::Dot(w, relativePosition);
if(dotProduct1 < 0.0F && dotProduct1 * dotProduct1 > combinedRadiusSq * wLengthSq) {
/* Project on cut-off circle. */
float wLength = Math::Sqrt(wLengthSq);
fvec2 unitW = w / wLength;
line.Direction = fvec2{unitW.Y, -unitW.X};
Check(!Math::IsNan(line.Direction));
u = (combinedRadius * RVOTimeHorizonInverted - wLength) * unitW;
} else {
/* Project on legs. */
float leg = Math::Sqrt(distSq - combinedRadiusSq);
if(Math::Cross(relativePosition, w) > 0.0F) {
/* Project on left leg. */
line.Direction =
fvec2{
relativePosition.X * leg - relativePosition.Y * combinedRadius,
relativePosition.X * combinedRadius + relativePosition.Y * leg}
/ distSq;
Check(!Math::IsNan(line.Direction));
} else {
/* Project on right leg. */
line.Direction =
-fvec2{
relativePosition.X * leg + relativePosition.Y * combinedRadius,
-relativePosition.X * combinedRadius + relativePosition.Y * leg}
/ distSq;
Check(!Math::IsNan(line.Direction));
}
float dotProduct2 = Math::Dot(relativeVelocity, line.Direction);
u = dotProduct2 * line.Direction - relativeVelocity;
}
} else {
/* Collision. Project on cut-off circle of time timeStep. */
var invTimeStep = (float)Time::TicksPerSecond;
/* Vector from cutoff center to relative velocity. */
fvec2 w = relativeVelocity - invTimeStep * relativePosition;
float wLength = Math::Length(w);
fvec2 unitW = w / wLength;
line.Direction = fvec2{unitW.Y, -unitW.X};
Check(!Math::IsNan(line.Direction));
u = (combinedRadius * invTimeStep - wLength) * unitW;
}
line.Position = Velocity + 0.5F * u;
ORCALines.Add(line);
}
void ProcessCollider(
entity Entity, const FCollider &Collider, const CStaticCollision *StaticCollision, const CDynamicCollision *DynamicCollision,
const CWorldTransform &WorldTransform, const CWorldTransformPreviousPosition &WorldTransformPreviousPosition,
const CMovementVelocity *MovementVelocity, CColliderCache &ColliderCache, const CDebugRenderSettings &DebugRenderSettings,
const CCamera2 &Camera) {
// TODO: Support runtime transitions (removing CStaticCollision and adding CDynamicCollision) through CColliderCache::WasStatic/WasDynamic.
var EntityId = Entity.id();
var ECS = Entity.world();
if(DebugRenderSettings.DebugRender) {
if(Math::DistanceSquared(WorldTransform.Position, Camera.Position) < Math::Square(DebugVisualizationRange)) {
if(const FColliderCircle *Circle = Collider) {
Debug::DebugCircle(ECS, Circle->Position, 0.0f, Circle->Radius, {0.0f, 0.0f, 1.0f, 0.6f}, 0.0f, 1.0f, false);
}
if(const FColliderRect *Rect = Collider) {
for(var Index = 0u; Index < 3u; ++Index) {
Debug::DebugLine(ECS, Rect->Polygon[Index + 0u], Rect->Polygon[Index + 1u], {0.0f, 0.0f, 1.0f, 0.6f}, 0.0f, 1.0f, false);
}
Debug::DebugLine(ECS, Rect->Polygon[3u], Rect->Polygon[0u], {0.0f, 0.0f, 1.0f, 0.6f}, 0.0f, 1.0f, false);
}
}
}
if(!IsPaused()) {
EColliderCacheCollisionCategory CollisionCategory;
if(StaticCollision) {
CollisionCategory = EColliderCacheCollisionCategory::Static;
} else if(DynamicCollision) {
CollisionCategory = EColliderCacheCollisionCategory::Dynamic;
} else {
CollisionCategory = EColliderCacheCollisionCategory::Trigger;
}
var SameCategory = ColliderCache.CollisionCategory == CollisionCategory;
var Dirty = !SameCategory || WorldTransform.Position != WorldTransformPreviousPosition.Position;
if(!Dirty && !DynamicCollision) {
return;
}
CellsRemain.Clear();
CellsAdded.Clear();
CellsRemoved.Clear();
TArrayView<ivec2> CellsRemainView;
if(Dirty) {
CellsRemoved = ColliderCache.Cells;
var Projection = Project(Collider);
for(var X = Projection.Start.X; X <= Projection.End.X; ++X) {
for(var Y = Projection.Start.Y; Y <= Projection.End.Y; ++Y) {
var Cell = ivec2{X, Y};
var Index = Array::Find(CellsRemoved, Cell);
if(Index != Array::NotFound) {
CellsRemoved.RemoveSwap(Index);
CellsRemain.Add(Cell);
} else {
CellsAdded.Add(Cell);
}
}
}
if(!CellsRemoved.IsEmpty()) {
ColliderCache.Cells.Clear();
ColliderCache.Cells.Add(CellsRemain);
}
ColliderCache.Cells.Add(CellsAdded);
CellsRemainView = View(CellsRemain);
} else {
CellsRemainView = View(ColliderCache.Cells);
}
ref CellsAddedView = CellsAdded;
ref CellsRemovedView = CellsRemoved;
// First remove from cells, to search less entries.
for(var Cell: CellsRemovedView) {
var Key = AssembleKey(Cell);
var Bucket = SpatialHash.ComputeBucket(Key);
FSpatialHashCell *SpatialHashCell = &SpatialHash.Get(Key, Bucket);
if(ColliderCache.CollisionCategory == EColliderCacheCollisionCategory::Static) {
Array::RemoveSwap(SpatialHashCell->StaticCollision, [EntityId](const FSpatialHashCollider &Collider) {
return Collider.EntityId == EntityId;
});
} else if(ColliderCache.CollisionCategory == EColliderCacheCollisionCategory::Dynamic) {
Array::RemoveSwap(SpatialHashCell->DynamicCollision, [EntityId](const FSpatialHashColliderDynamic &Collider) {
return Collider.EntityId == EntityId;
});
} else if(ColliderCache.CollisionCategory == EColliderCacheCollisionCategory::Trigger) {
Array::RemoveSwap(SpatialHashCell->TriggerCollision, [EntityId](const FSpatialHashCollider &Collider) {
return Collider.EntityId == EntityId;
});
}
}
var Velocity = MovementVelocity ? MovementVelocity->Velocity
: (Math::IsNan(WorldTransformPreviousPosition.Position)
? fvec2{0.0f, 0.0f}
: WorldTransform.Position - WorldTransformPreviousPosition.Position);
if(SameCategory) {
if(DynamicCollision) {
for(var Cell: CellsRemainView) {
var Key = AssembleKey(Cell);
ref Dynamic = SpatialHash[Key].DynamicCollision;
var Index = Array::Find(Dynamic, [EntityId](const FSpatialHashColliderDynamic &Collider) {
return Collider.EntityId == EntityId;
});
ref ColliderDynamic = Dynamic[Index];
ColliderDynamic.Velocity = Velocity;
if(FColliderCircle *ColliderCircle = ColliderDynamic.Collider) {
ColliderCircle->Position = WorldTransform.Position;
} else {
Prevent();
}
}
}
} else {
// Category changed, we have to move the Collider between storages of each cell.
for(var Cell: CellsRemainView) {
var Key = AssembleKey(Cell);
// Remove from old (if any).
if(ColliderCache.CollisionCategory == EColliderCacheCollisionCategory::Static) {
ref Static = SpatialHash[Key].StaticCollision;
var Index = Array::Find(Static, [EntityId](const FSpatialHashCollider &Collider) {
return Collider.EntityId == EntityId;
});
Static.RemoveSwap(Index);
} else if(ColliderCache.CollisionCategory == EColliderCacheCollisionCategory::Dynamic) {
ref Dynamic = SpatialHash[Key].DynamicCollision;
var Index = Array::Find(Dynamic, [EntityId](const FSpatialHashColliderDynamic &Collider) {
return Collider.EntityId == EntityId;
});
Dynamic.RemoveSwap(Index);
} else if(ColliderCache.CollisionCategory == EColliderCacheCollisionCategory::Trigger) {
ref Trigger = SpatialHash[Key].TriggerCollision;
var Index = Array::Find(Trigger, [EntityId](const FSpatialHashCollider &Collider) {
return Collider.EntityId == EntityId;
});
Trigger.RemoveSwap(Index);
}
// Add to new (if any).
if(CollisionCategory == EColliderCacheCollisionCategory::Static) {
SpatialHash[Key].StaticCollision.Add({Collider, Entity.id()});
} else if(CollisionCategory == EColliderCacheCollisionCategory::Dynamic) {
SpatialHash[Key].DynamicCollision.Add({Collider, Velocity, Entity.id()});
} else if(CollisionCategory == EColliderCacheCollisionCategory::Trigger) {
SpatialHash[Key].TriggerCollision.Add({Collider, Entity.id()});
}
}
}
for(var Cell: CellsAddedView) {
var Key = AssembleKey(Cell);
var Bucket = SpatialHash.ComputeBucket(Key);
FSpatialHashCell *SpatialHashCell;
if(var Existing = SpatialHash.Find(Key, Bucket)) {
SpatialHashCell = Existing;
} else {
SpatialHashCell = &SpatialHash.GetOrCreate(Key, Bucket);
SpatialHashCell->CellPosition = fvec2{((float)Cell.X + 0.5f) * CellSizeFloat, ((float)Cell.Y + 0.5f) * CellSizeFloat};
}
if(CollisionCategory == EColliderCacheCollisionCategory::Static) {
SpatialHashCell->StaticCollision.Add({Collider, Entity.id()});
} else if(CollisionCategory == EColliderCacheCollisionCategory::Dynamic) {
SpatialHashCell->DynamicCollision.Add({Collider, Velocity, Entity.id()});
} else if(CollisionCategory == EColliderCacheCollisionCategory::Trigger) {
SpatialHashCell->TriggerCollision.Add({Collider, Entity.id()});
}
}
ColliderCache.CollisionCategory = CollisionCategory;
}
}
uint64 AssembleKey(ivec2 Cell) {
return *(const uint64 *)&Cell;
}
uint64 AssembleKey(fvec2 Position) {
ivec2 Cell;
Cell.X = (int32)Position.X / CellSize;
Cell.Y = (int32)Position.Y / CellSize;
return AssembleKey(Cell);
}
FSpatialProjectionResult Project(const FCollider &Collider) {
FSpatialProjectionResult Result;
if(const FColliderCircle *Circle = Collider) {
Result.Start = {(int32)(Circle->Position.X - Circle->Radius), (int32)(Circle->Position.Y - Circle->Radius)};
Result.End = {(int32)(Circle->Position.X + Circle->Radius), (int32)(Circle->Position.Y + Circle->Radius)};
} else if(const FColliderRect *Rect = Collider) {
ref Polygon = Rect->Polygon;
Result.Start = Result.End = {(int32)Polygon[0].X, (int32)Polygon[0].Y};
for(var Index = 1u; Index < 4u; ++Index) {
var Point = ivec2{(int32)Polygon[Index].X, (int32)Polygon[Index].Y};
Result.Start.X = Math::Min(Result.Start.X, Point.X);
Result.Start.Y = Math::Min(Result.Start.Y, Point.Y);
Result.End.X = Math::Max(Result.End.X, Point.X);
Result.End.Y = Math::Max(Result.End.Y, Point.Y);
}
} else {
Prevent();
}
if(Result.Start.X < 0) {
Result.Start.X -= CellSize;
}
if(Result.End.X < 0) {
Result.End.X -= CellSize;
}
if(Result.Start.Y < 0) {
Result.Start.Y -= CellSize;
}
if(Result.End.Y < 0) {
Result.End.Y -= CellSize;
}
Result.Start /= CellSize;
Result.End /= CellSize;
return Result;
}
} // namespace
} // namespace SE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment