@JobLeonard, your sketch illustrates the method I use nicely

I don't see the need for the convex hull though - I just use a line/vector distance to check which point that the rope hit first (on your sketch that top left one) and because that point will be convex and far out from the middle, it'll be one of the convex hull vertices either way, without having to explicitly calculate it. The concave points (such as the one in that crevice on the shape in your sketch) there's really no reason to bother with at all, because as long as the tips of the rope have terrain collision those can never end up inside a sweep.
@Christian, TheWing, yep, 45 unless you hold up, then straight up. And a little bit of auto aim - if the original aim direction seems to be all empty of terrain it'll check a few angles to the sides as well and see if any of those hit something.
@Paul, wow yeah, that looks super similar! Let me know what was and wasn't the same in the code!
public class Rope
{
public Room room;
public Vector2 A, B, lastA, lastB;
public float totalLength;
public List<Corner> bends;
float thickness;
public RopeDebugVisualizer visualizer;
public List<Corner> corners;
public Vector2 AConnect
{
get
{
if (bends.Count == 0) return B;
else return bends[0].pos;
}
}
public Vector2 BConnect
{
get
{
if (bends.Count == 0) return A;
else return bends[bends.Count - 1].pos;
}
}
public struct Corner
{
public FloatRect.CornerLabel dir;
public Vector2 pos;
public Corner(FloatRect.CornerLabel dir, Vector2 pos)
{
this.dir = dir;
this.pos = pos;
}
}
public Rope(Room room, Vector2 initA, Vector2 initB, float thickness)
{
this.room = room;
A = initA;
lastA = initA;
B = initB;
lastB = initB;
totalLength = Vector2.Distance(initA, initB);
bends = new List<Corner>();
corners = new List<Corner>();
this.thickness = thickness;
//finds all the corners and stores them in a list. This could also be done locally on the fly, for very large spaces with lots of corners that might be preferable performance-wise.
for (int x = 0; x < room.TileWidth; x++)
for (int y = 0; y < room.TileHeight; y++)
if (room.GetTile(x, y).Solid) {
if (!room.GetTile(x - 1, y).Solid) {
if (!room.GetTile(x, y - 1).Solid && !room.GetTile(x - 1, y - 1).Solid)
corners.Add(new Corner(FloatRect.CornerLabel.D, room.MiddleOfTile(x, y) + new Vector2(-10f - thickness, -10f - thickness)));
if (!room.GetTile(x, y + 1).Solid && !room.GetTile(x - 1, y + 1).Solid)
corners.Add(new Corner(FloatRect.CornerLabel.A, room.MiddleOfTile(x, y) + new Vector2(-10f - thickness, 10f + thickness)));
}
if (!room.GetTile(x + 1, y).Solid) {
if (!room.GetTile(x, y - 1).Solid && !room.GetTile(x + 1, y - 1).Solid)
corners.Add(new Corner(FloatRect.CornerLabel.C, room.MiddleOfTile(x, y) + new Vector2(10f + thickness, -10f - thickness)));
if (!room.GetTile(x, y + 1).Solid && !room.GetTile(x + 1, y + 1).Solid)
corners.Add(new Corner(FloatRect.CornerLabel.B, room.MiddleOfTile(x, y) + new Vector2(10f + thickness, 10f + thickness)));
}
}
// visualizer = new RopeDebugVisualizer(this);
}
public void Reset()
{
bends.Clear();
}
public void Update(Vector2 newA, Vector2 newB)
{
lastA = A;
lastB = B;
A = newA;
B = newB;
//sweep terrain to add bend points
if (bends.Count == 0)
CollideWithCorners(lastA, A, lastB, B, 0, 0);
else {
CollideWithCorners(BConnect, BConnect, lastB, B, bends.Count, 0);
CollideWithCorners(lastA, A, AConnect, AConnect, 0, 0);
}
//delete bend points where the rope has come free of the corner
if (bends.Count > 0) {//could optimize here by making it only check first and last bend point
List<int> deleteBends = new List<int>();
for (int i = 0; i < bends.Count; i++) {
Vector2 prev = A;
Vector2 nxt = B;
if (i > 0) prev = bends[i - 1].pos;
if (i < bends.Count - 1) nxt = bends[i + 1].pos;
if (!DoesLineOverlapCorner(prev, nxt, bends[i])) {
deleteBends.Add(i);
}
}
for (int i = deleteBends.Count - 1; i >= 0; i--)
bends.RemoveAt(deleteBends[i]);
}
//Calculating new total length of rope
if (bends.Count == 0)
totalLength = Vector2.Distance(A, B);
else {
totalLength = Vector2.Distance(A, AConnect) + Vector2.Distance(BConnect, B);
for (int i = 1; i < bends.Count; i++)
totalLength += Vector2.Distance(bends[i - 1].pos, bends[i].pos);
}
if (visualizer != null) visualizer.Update();
}
private void CollideWithCorners(Vector2 la, Vector2 a, Vector2 lb, Vector2 b, int bend, int recursion)
{
Corner? firstCollision = null;
float firstCollisionDist = float.MaxValue;
foreach (Corner c in corners)
if (DoesLineOverlapCorner(a, b, c)
&& c.pos != la && c.pos != a && c.pos != lb && c.pos != b
// && Custom.BetweenLines(c.pos, la, a, lb, b) && Custom.BetweenLines(c.pos, la, lb, a, b)//old sweep function that would give some false positives
&& (Custom.PointInTriangle(c.pos, a, la, b) || Custom.PointInTriangle(c.pos, a, lb, b) || Custom.PointInTriangle(c.pos, a, la, lb) || Custom.PointInTriangle(c.pos, la, lb, b))
&& Mathf.Abs(Custom.DistanceToLine(c.pos, la, lb)) < Mathf.Abs(firstCollisionDist)) {
firstCollision = c;
firstCollisionDist = Custom.DistanceToLine(c.pos, lastA, lastB);
}
if (firstCollision.HasValue) {
Vector2 bendPoint = firstCollision.Value.pos;
bends.Insert(bend, firstCollision.Value);
Vector2 divPoint = Custom.ClosestPointOnLine(la, lb, bendPoint);
CollideWithCorners(divPoint, bendPoint, lb, b, bend + 1, recursion + 1);
CollideWithCorners(la, a, divPoint, bendPoint, bend, recursion + 1);
}
}
public bool DoesLineOverlapCorner(Vector2 l1, Vector2 l2, Corner corner)
{
IntVector2 cornerDir = new IntVector2((corner.dir == FloatRect.CornerLabel.A || corner.dir == FloatRect.CornerLabel.D) ? -1 : 1,
(corner.dir == FloatRect.CornerLabel.A || corner.dir == FloatRect.CornerLabel.B) ? 1 : -1);
if (l1.y != l2.y)
if ((cornerDir.x < 0 && (Custom.HorizontalCrossPoint(l1, l2, corner.pos.y).x < corner.pos.x))
|| (cornerDir.x > 0 && (Custom.HorizontalCrossPoint(l1, l2, corner.pos.y).x > corner.pos.x)))
return false;
if (l1.x != l2.x)
if ((cornerDir.y < 0 && (Custom.VerticalCrossPoint(l1, l2, corner.pos.x).y < corner.pos.y))
|| (cornerDir.y > 0 && (Custom.VerticalCrossPoint(l1, l2, corner.pos.x).y > corner.pos.y)))
return false;
return true;
}
}