Not only is the grapple hook fun to use in game, its an interesting challenge to implement in code. There are essentially three main parts:
1. The "rope" needs to shoot out from the player and catch on some surface of the world. This can either be special tiles, or any sort of tile, though in some games this may lead to exploits.
2. Once attached, the player swings in motion similar to a pendulum.
3. When you release, you swing in the direction you were traveling, potentially covering gaps. The player can also re-attach the grapple, and swing horizontally ala Tarzan.
Similar to the jump logic, the first thing I did was create a data structure to hold all the variables we need to keep track of during the grapple swing:
public struct GrappleStruct { public int mode; //0=shooting, 1=attached public SwingCurveStruct swingCurve; public Vector2 position; //position of "hook" public TimeSpan GrappleTimer; public Vector2 launchVector; public Vector2 FireVector; public float RopeLength; public float fireSpeed; public Vector2 aimVector; } public struct SwingCurveStruct { public Curve YCurve; public Curve XCurve; }
SwingCurveStruct is also a data type that comes in handy - it holds two Curve objects. The Curve class allows you to interpolate over a curve given a set of discrete points. So instead of defining complex movement purely by physics formulas, you can numerically determine a few key points along the curve, plug them into a Curve class and then approximate the curve easily. We use a SwingCurveStruct to hold the points in the swing itself.
GrappleTimer tracks how much time the player has been "attached" to the grapple, so we can evaluate the correct position in the swing.
launchVector defines the vector when the player releases from the grapple hook, and lets the player "fling" up in the air.
FireVector is the direction the player is shooting the grapple hook. Right now the "rope" only goes in an upward 45 degrees for ease of use, however this value could be set by the player.
The RopeLength and fireSpeed define how far and how fast the rope itself can shoot out. These values are set when the grapple struct is first created. Longer and faster ropes make it much easier to attach to surfaces, and vice-versa.
The bulk of the logic for the grapple is done in the DoGrapple function:
public bool DoGrapple(bool artifactPushed, GameTime gameTime, ref Vector2 position, ref Vector2 Force1Vector, ref Vector2 velocity, Level level, SpriteEffects flip) { float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds; bool isSwinging = false; if (artifactPushed) { if (playerGrappleStruct.mode == 0) { if (playerGrappleStruct.FireVector.X == 0 && playerGrappleStruct.FireVector.Y == 0) { if (playerGrappleStruct.aimVector.X == 0 && playerGrappleStruct.aimVector.Y == 0) { if (flip == SpriteEffects.FlipHorizontally) { playerGrappleStruct.FireVector = new Vector2(1f, -1f); } else { playerGrappleStruct.FireVector = new Vector2(-1f, -1f); } } else { playerGrappleStruct.FireVector = new Vector2(playerGrappleStruct.aimVector.X, playerGrappleStruct.aimVector.Y); } } playerGrappleStruct.position += (playerGrappleStruct.FireVector * elapsed * playerGrappleStruct.fireSpeed) + (velocity * elapsed); if (Helper.getDistance(position, playerGrappleStruct.position) > playerGrappleStruct.RopeLength) { playerGrappleStruct.position = position + new Vector2(0, -25); } GrappleCollision(level,position,velocity); isSwinging = false; } if (playerGrappleStruct.mode == 1) { playerGrappleStruct.GrappleTimer += gameTime.ElapsedGameTime; float maxTime = playerGrappleStruct.swingCurve.XCurve.Keys[playerGrappleStruct.swingCurve.XCurve.Keys.Count - 1].Position; float newPositionX = playerGrappleStruct.swingCurve.XCurve.Evaluate((float)playerGrappleStruct.GrappleTimer.TotalSeconds % maxTime); float newPositionY = playerGrappleStruct.swingCurve.YCurve.Evaluate((float)playerGrappleStruct.GrappleTimer.TotalSeconds % maxTime); playerGrappleStruct.launchVector = setSwingVelocity(new Vector2(newPositionX, newPositionY), position, (float)gameTime.ElapsedGameTime.TotalSeconds); playerGrappleStruct.LaunchTimer = TimeSpan.FromSeconds(1); playerGrappleStruct.launchCurve = setLaunchCurveStruct(playerGrappleStruct.launchVector); position.X = newPositionX; position.Y = newPositionY; isSwinging = true; } } else { if (playerGrappleStruct.mode == 1) { playerGrappleStruct.mode = 0; Force1Vector = playerGrappleStruct.launchVector; } //reset the rope; playerGrappleStruct.position = position + new Vector2(0, -25); playerGrappleStruct.FireVector = new Vector2(0f, 0f); isSwinging = false; } return isSwinging; }
The function has three main sections, corresponding to the three parts listed above.
1. If we're shooting the grapple hook (artifactPressed) and in mode 0, then increment the location of the "hook" and check if we've collided with any surfaces.
We use the GrappleCollide function to check:
private void GrappleCollision(Level level, Vector2 position, Vector2 velocity) { Vector2 temp; if (Collision.getLineCollision(position + new Vector2(0, -25), playerGrappleStruct.position, level, out temp)) { if ((position.Y - 25) > temp.Y && Math.Abs(position.X - temp.X) > 50) //make sure we only grappling to points above us { playerGrappleStruct.position = temp; playerGrappleStruct.mode = 1; playerGrappleStruct.swingCurve = setGrappleSwingCurve(playerGrappleStruct.position, position + new Vector2(0, -25), velocity); playerGrappleStruct.GrappleTimer = TimeSpan.FromSeconds(0f); } } }
getLineCollision essentially gets a list of all tiles that are overlapped by a line connecting the player to the hook. We then traverse this list, moving outward from the player, and if we hit an Impassable or Platform tile, we return true, we're connected.
Once we're connected, we change to mode 1 and we have to calculate the swing curve:
/* * given the initial force vector (x, y), and the angle of attachment, calculate the swing curve of the grapple, using pendulum physics * pendulum forumula * * O'' = -g/R sin O * angular acceleration = - gravity / length of rope * sin angle * angle (O=vertical) * */ private SwingCurveStruct setGrappleSwingCurve(Vector2 origin, Vector2 playerPosition, Vector2 initVelocity) { //length of rope float length = (float)Math.Sqrt(Math.Pow((origin.X - playerPosition.X), 2) + Math.Pow((origin.Y - playerPosition.Y), 2)); Vector2 slope = (playerPosition - origin); slope.Normalize(); slope = new Vector2(slope.X, slope.Y * -1); //get start angle float angleStart = (float)(Math.Atan2((double)slope.Y, (double)slope.X)); float initVelocityRadians = getInitVelocity(length, origin, playerPosition, initVelocity); float time = 0f; float angleAcc = 0f; float angleVel = initVelocityRadians; float curAngle = angleStart; SwingCurveStruct retval = new SwingCurveStruct(); retval.XCurve = new Curve(); retval.YCurve = new Curve(); retval = addCurvePosition(retval, time, origin, length, curAngle); //iterate through and set curve keys for (float f = time; f < 20f; f += .1f) { angleAcc = -(GravityAcceleration / length) * (float)Math.Sin(curAngle + (float)Math.PI / 2); angleVel += angleAcc * .1f; curAngle += angleVel * .1f; retval = addCurvePosition(retval, f, origin, length, curAngle); } return retval; }This function uses the formula for a pendulum to create Curve objects that the player will move on while attached. Here's a great site that walked me through pendulum physics: http://www.myphysicslab.com/pendulum1.html
First, we have to determine the initial angle the player is attached:
float angleStart = (float)(Math.Atan2((double)slope.Y, (double)slope.X));Next, we set an initial velocity. This basically gives the player a little more momentum in the swing if they are moving when they attach, and makes "Tarzan" swings a little more powerful.
private float getInitVelocity(float length, Vector2 origin, Vector2 playerPosition, Vector2 initVelocity) { //pythagorean? float velocityDist = (float)Math.Sqrt((initVelocity.X * initVelocity.X) + (initVelocity.Y * initVelocity.Y)); float cosx = Math.Abs((length * length + length * length - velocityDist * velocityDist) / (2 * length * length)); float initVelocityRadians = (float)Math.Acos(cosx); if (float.IsNaN(initVelocityRadians)) initVelocityRadians = 0f; //unit circle goes counter clockwise, so if we are swinging the otherway, then multiple by -1 if (initVelocity.X < 0) initVelocityRadians *= -1; return initVelocityRadians; }Last, we just need to iterate the points of a pendulum into the Curve structs, The pendulum takes the following forumla:
angleAcc = -(GravityAcceleration / length) * (float)Math.Sin(curAngle + (float)Math.PI / 2);Once we have the angular acceleration, we can determine the angular velocity, and thus the current position in radians. From there, we can easily calculate the current position along the circumference of a circle (the "hook" being the origin).
//gets the x,y vector at the end of the pendulum, given origin, length and radians private Vector2 getCurvePosition(Vector2 origin, float length, float radians) { return new Vector2((float)(origin.X + length * Math.Cos(radians)), (float)(origin.Y + length * (float)Math.Sin(radians) * -1)); }Note: the swing algorithm breaks down if the player attaches to a surface at less than the horizontal plane. This is because the swing follows the path of a pendulum, and if the angle is greater than the horizontal, the rope will be "stiff", and the player could even use it as a sort of stilt. If you want full elastic grapple hooks, some different physics logic is needed
2. Once we are attached, we just need to determine our current position on the swing curve.
float newPositionX = playerGrappleStruct.swingCurve.XCurve.Evaluate((float)playerGrappleStruct.GrappleTimer.TotalSeconds % maxTime); float newPositionY = playerGrappleStruct.swingCurve.YCurve.Evaluate((float)playerGrappleStruct.GrappleTimer.TotalSeconds % maxTime);Note: We're also doing a special collision check at this point to make sure the player can't swing through walls. Because we aren't adjusting the player velocity, and instead directly updating the player position, there is the chance that the player could move so fast they could warp through walls (given a long enough rope, or a strong enough GravityAcceleration). So lots of tweaking/testing is needed to find the right balance.
3. Once the player lets go (artifactPushed == false), we need to apply the launch velocity. We actually set the launch velocity while we are connected with the following function:
private Vector2 setSwingVelocity(Vector2 position1, Vector2 position2, float timeDiff) { return (position1 - position2) * (float)(1.0 / timeDiff) * 20; }
We find the difference in position during the swing, then multiply by 20. The 20 is an arbitrary value, and can be adjusted to set how much the player "flings" after they release the grapple hook. This is another case where if you use realistic physics, the gameplay isn't as fun as using exaggerated calculations.
Last, we apply gravity as normal. The launch vector is saved in Force1Vector, and is applied along with the other vectors.
So that's the basic grapple hook, probably the most difficult "movement tool" being implement in Platform Hack. Next time, it's onto something completely different - procedural maze generation!