This is probably the problem I struggled with most in the development of both major versions of Scrolling Game Development Kit. So here I will try to describe what the central function that addresses your problem in SGDK2 is doing. It's all wrapped up in a function called ReactToSolid
. This function actually doesn't deal strictly with rectangles because SGDK2 allows tiles that are shaped like slopes too.
Before diving into the code, I should explain some of the variables you'll see:
x
- The raw horizontal coordinate of the left side of a sprite. I say raw because this is not an integer and can store a position between pixels to help track, for example, a slow moving sprite that hasn't reached the next pixel yet.
y
- The raw vertical coordinate of the top of the sprite.
PixelX
- The truncated horizontal coordinate (round down to integer)
PixelY
- The truncated vertical coordinate
dx
- The horizontal velocity of the sprite used to determine where the sprite should move next
dy
- The vertical velocity of the sprite
ProposedPixelX
- The next value of PixelX
based on current values of x
and dx
(in other words, x+dx
rounded down)
ProposedPixelY
- The next value of PixelY
The rest of the variables will be explained afterward.
And a some general points about this function:
- This function alters the dx and dy properties of a sprite so that when it moves (adjusts its x and y properties by adding dx and dy) it will avoid solid areas of the background.
- This function assumes that the sprite is not already overlapping any solids because once the sprite is overlapping a solid, you've lost a lot of information about how you should avoid the solid based on where you came from. If it is already overlapping, the behavior tends to be that solid edges going through the sprite are ignored.
The code:
public virtual bool ReactToSolid()
{
Debug.Assert(this.isActive, "Attempted to execute ReactToSolid on an inactive sprite");
if (m_solidity == null)
throw new System.ApplicationException("Attempted to execute ReactToSolid on sprite without solidity defined");
bool hit = false;
double dyOrig = dy;
double dxOrig = dx;
int ProposedPixelY2 = (int)Math.Ceiling(y + dy);
int SolidPixelWidth = SolidWidth + (int)Math.Ceiling(x) - PixelX;
if (dy > 0)
{
int ground = layer.GetTopSolidPixel(new System.Drawing.Rectangle(PixelX, PixelY + SolidHeight, SolidPixelWidth, ProposedPixelY2 - PixelY), m_solidity);
if (ground != int.MinValue)
{
// Do integer arithmetic before double otherwise strange rounding seems to happen
dy = ground - SolidHeight - y;
hit = true;
}
}
else if (dy < 0)
{
int ceiling = layer.GetBottomSolidPixel(new System.Drawing.Rectangle(PixelX, ProposedPixelY, SolidPixelWidth, PixelY - ProposedPixelY), m_solidity);
if (ceiling != int.MinValue)
{
// Do integer arithmetic before double otherwise strange rounding seems to happen
dy = ceiling + 1 - y;
hit = true;
}
}
if (dx > 0)
{
int ProposedPixelX2 = (int)Math.Ceiling(x + dx);
int PixelX2 = (int)Math.Ceiling(x);
int rightwall = layer.GetLeftSolidPixel(new System.Drawing.Rectangle(PixelX2 + SolidWidth, ProposedPixelY, ProposedPixelX2 - PixelX2, SolidHeight), m_solidity);
bool hitWall = false;
if (rightwall != int.MinValue)
{
int maxSlopeProposedY = (int)(y + dy - dx);
int slopedFloor = layer.GetTopSolidPixel(new System.Drawing.Rectangle(PixelX2 + SolidWidth, maxSlopeProposedY + SolidHeight, ProposedPixelX2 - PixelX2, ProposedPixelY - maxSlopeProposedY), m_solidity);
if (slopedFloor != int.MinValue)
{
int ceiling = layer.GetBottomSolidPixel(new System.Drawing.Rectangle(PixelX2, slopedFloor - SolidHeight, SolidWidth, ProposedPixelY + SolidHeight - slopedFloor), m_solidity);
if ((ceiling == int.MinValue) && (RidingOn == null))
{
int rightwall2 = layer.GetLeftSolidPixel(new System.Drawing.Rectangle(PixelX2 + SolidWidth, slopedFloor - SolidHeight, ProposedPixelX2 - PixelX2, SolidHeight), m_solidity);
if (rightwall2 == int.MinValue)
// Do integer arithmetic before double otherwise strange rounding seems to happen
dy = dyOrig = slopedFloor - SolidHeight - 1 - y;
else
hitWall = true;
}
else
hitWall = true;
}
else
{
maxSlopeProposedY = (int)(y + dy + dx);
int slopedCeiling = layer.GetBottomSolidPixel(new System.Drawing.Rectangle(PixelX2 + SolidWidth, ProposedPixelY, ProposedPixelX2 - PixelX2, maxSlopeProposedY - ProposedPixelY), m_solidity);
if (slopedCeiling != int.MinValue)
{
slopedCeiling++;
int floor = layer.GetTopSolidPixel(new System.Drawing.Rectangle(PixelX2, ProposedPixelY + SolidHeight, SolidWidth, slopedCeiling - ProposedPixelY), m_solidity);
if ((floor == int.MinValue) && (RidingOn == null))
{
int rightwall2 = layer.GetLeftSolidPixel(new System.Drawing.Rectangle(PixelX2 + SolidWidth, slopedCeiling, ProposedPixelX2 - PixelX2, SolidHeight), m_solidity);
if (rightwall2 == int.MinValue)
dy = dyOrig = slopedCeiling - y;
else
hitWall = true;
}
else
hitWall = true;
}
else
hitWall = true;
}
if (hitWall)
{
// Do integer arithmetic before double otherwise strange rounding seems to happen
dx = rightwall - SolidWidth - x;
}
hit = true;
}
}
else if (dx < 0)
{
int leftwall = layer.GetRightSolidPixel(new System.Drawing.Rectangle(ProposedPixelX, ProposedPixelY, PixelX - ProposedPixelX, SolidHeight), m_solidity);
bool hitWall = false;
if (leftwall != int.MinValue)
{
int maxSlopeProposedY = (int)(y + dy + dx);
int slopedFloor = layer.GetTopSolidPixel(new System.Drawing.Rectangle(ProposedPixelX, maxSlopeProposedY + SolidHeight, PixelX - ProposedPixelX, ProposedPixelY - maxSlopeProposedY), m_solidity);
if (slopedFloor != int.MinValue)
{
int ceiling = layer.GetBottomSolidPixel(new System.Drawing.Rectangle(PixelX, slopedFloor - SolidHeight, SolidWidth, ProposedPixelY + SolidHeight - slopedFloor), m_solidity);
if ((ceiling == int.MinValue) && (RidingOn == null))
{
int leftwall2 = layer.GetRightSolidPixel(new System.Drawing.Rectangle(ProposedPixelX, slopedFloor - SolidHeight, PixelX - ProposedPixelX, SolidHeight), m_solidity);
if (leftwall2 == int.MinValue)
// Do integer arithmetic before double otherwise strange rounding seems to happen
dy = dyOrig = slopedFloor - SolidHeight - 1 - y;
else
hitWall = true;
}
else
hitWall = true;
}
else
{
maxSlopeProposedY = (int)(y + dy - dx);
int slopedCeiling = layer.GetBottomSolidPixel(new System.Drawing.Rectangle(ProposedPixelX, ProposedPixelY, PixelX - ProposedPixelX, maxSlopeProposedY - ProposedPixelY), m_solidity);
if (slopedCeiling != int.MinValue)
{
slopedCeiling++;
int floor = layer.GetTopSolidPixel(new System.Drawing.Rectangle(PixelX, ProposedPixelY + SolidHeight, SolidWidth, slopedCeiling - ProposedPixelY), m_solidity);
if ((floor == int.MinValue) && (RidingOn == null))
{
int leftwall2 = layer.GetRightSolidPixel(new System.Drawing.Rectangle(ProposedPixelX, slopedCeiling, PixelX - ProposedPixelX, SolidHeight), m_solidity);
if (leftwall2 == int.MinValue)
dy = dyOrig = slopedCeiling - y;
else
hitWall = true;
}
else
hitWall = true;
}
else
hitWall = true;
}
if (hitWall)
{
// Do integer arithmetic before double otherwise strange rounding seems to happen
dx = leftwall + 1 - x;
}
hit = true;
}
}
dy = dyOrig;
int ProposedSolidPixelWidth = SolidWidth + (int)Math.Ceiling(x + dx) - ProposedPixelX;
if (dy > 0)
{
ProposedPixelY2 = (int)Math.Ceiling(y + dy);
int ground = layer.GetTopSolidPixel(new System.Drawing.Rectangle(ProposedPixelX, PixelY + SolidHeight, ProposedSolidPixelWidth, ProposedPixelY2 - PixelY), m_solidity);
if (ground != int.MinValue)
{
// Do integer arithmetic before double otherwise strange rounding seems to happen
dy = ground - SolidHeight - y;
hit = true;
}
}
else if (dy < 0)
{
int ceiling = layer.GetBottomSolidPixel(new System.Drawing.Rectangle(ProposedPixelX, ProposedPixelY, ProposedSolidPixelWidth, PixelY - ProposedPixelY), m_solidity);
if (ceiling != int.MinValue)
{
// Do integer arithmetic before double otherwise strange rounding seems to happen
dy = ceiling + 1 - y;
hit = true;
}
}
if (hit && !double.IsNaN(LocalDX))
LocalDX += dx - dxOrig;
return hit;
}
- The first major check for
dy>0
and dy<0
is to determine whether we should
be looking for a floor versus a ceiling to determine whether the
sprite will be blocked from moving downward or upward.
- Inside that block, the sprite's vertical velocity is adjusted to stop at the
floor or ceiling that it is about to hit.
GetTopSolidPixel
and
GetBottomSolidPixel
are functions that return the vertical coordinate of
the topmost or bottom-most solid pixel within the specified rectangle.
Notice that we're not including the sprite rectangle itself in the area
being checked because the sprite should not be overlapping solid already.
We're just checking the rectangle that the edge of the sprite is moving
through. Also notice that we use a variation ProposedPixelY2
because we
want to be sure to check the bottom of the sprite's lowest pixel when
looking downward to avoid rounding errors.
- In the next blocks concerning
dx>0
and dx<0
we are basically doing the
same check horizontally instead of vertically. But because hills add a lot
of complexity, there's a lot more code. Since you're probably not concerned
with hills much, I'll try to focus on the rectangular portion of the code.
- When we check for
int.MinValue
we're basically checking
whether no solid pixel edge was found on the background layer in the
requested rectangle. int.MinValue
is returned when no solid pixel was
found. So basically, if we skip all the slope logic, we are matching the
edge of the sprite to the left or right wall toward which it is headed if
one was found. (Technically we're adjusting the sprite's dx so that it will
match after its position is next updated.) The vast majority of the code
here deals with sloped floors and ceilings, which I will ignore.
- Next we are restoring the original vertical velocity (
dy = dyOrig
) and
re-applying the ceiling and ground check, but this time taking into account
not only the vertical movement of the sprite, but also the corrected horizontal movement. We also account for the fact that the sprite is 1 pixel wider when sitting between pixels. Honestly, this code has gotten
so refined over the weeks of trial and error that formed it that I can't
necessarily explain in detail all the reasons this had to be done, but
here's my best attempt:
- The earlier ground/ceiling check needed to be done ignoring the horizontal velocity because we weren't sure what it would be yet. But we needed a good approximation of the corrected vertical velocity in order to compute a good horizontal velocity. Landing on a floor may cause the sprite to land at a height where it can hit a wall even though it would not have hit that wall if the floor weren't there (if the sprite is taller than 1 tile or is approaching a sloped ceiling).
- The later ground/ceiling check needed to be done so that the sprite's vertical velocity would be properly adjusted after taking into account the effect of walls on the expected horizontal location of the sprite.
- The original vertical velocity
dyOrig
is used as a starting point
because the earlier check that was done was not conclusive and we want to
basically start over now that we know a good horizontal velocity.
- The last bit of code dealing with
LocalDX
only applies when the sprite is
riding on top of another sprite (LocalDX
is the velocity of the sprite
relative to the sprite on which it is riding). The relative velocity is
updated according the the change that was applied to the absolute velocity.
This prevents platforms from being used as a means to travel through walls.
(The vertical equivalent of this logic is in a different function because
hitting a floor or ceiling while riding a platform causes the sprite to stop
riding.)
while(areColliding(movingRect, stillRect, direction) {moveOneUnit(movingRect, getOpposite(direction))}
for an ugly sudo code algorithm. Keep in mind this is a naive approach assuming only one rectangle is moving. It would need to factor both rectangles velocities and directions if you have 2 moving rectangles. Happy hunting for a solution. \$\endgroup\$