I'm going to discuss how I like to implement a rope-swinging gameplay mechanic. When I started working on Energy Hook I mostly did everything with my own custom code. I believe it's possible to simply go into Unity and use the Configurable Joint to do some very similar stuff, but at the time it wasn't available. I'm pretty sure this has been the right call, anyway, because it gives me control over everything - and I can share it with you.
The fundamentals of doing swinging the way I did - using constraints - are actually quite simple. (Hope you're not disappointed when you see what's under the hood.) It works the same whether you're making a 2D game or 3D game, it's just that the vectors are different, so I'll start off with 2D and then discuss some wrinkles when going to three dimensions.
This article also assumes you're using a game engine like Unity that can do a lot of the work for you, like raycasting against geometry and orienting a character, with simple function calls. If you're using your own engine, you may have to do that math yourself.
Physical Simulations vs. Canned Movement
There are two ways game simulations often work. Most third person games have canned everything; canned animations and canned movements so everything looks perfect, the character's feet don't slide, straight from the animator's animation program to us. The alternative is a much more physical simulation - you're simulating physics: acceleration, velocity, gravity - a lot of first-person games do this, but third-person games tend to avoid it, because it's much easier to have the character's feet slide and the physical stuff not match the animation.
If you want to do a non-canned rope swing (as opposed to a canned rope swing, as seen in the very first Pitfall, or the early Spider-Man games like Neversoft's on the PSX), a rope-swing that's actually a physical simulation and can therefore maybe have freedom and nuance and the visceral feeling that real gravity and momentum can give you, then you're going to want to do it the second way - have everything be a physical simulation, and avoid doing canned things that kill the character's momentum. Then, everything will flow.
That's the way I do things with Energy Hook.
There are a ton of articles out there on how to simulate physics - here's one with a cool Flash demo built in. You can do Euler integration or Verlet integration, it doesn't really matter. (I won't teach you about Euler vs. Verlet here, but let me just say that the concepts aren't as scary as they sound.)
Let's assume we do the bone simple Euler integration. Our character's game timestep might look like this, where acceleration is determined by gravity and how you're pushing on the stick:
1 2 | avatar.velocity = avatar.velocity + avatar.acceleration * deltaT; avatar.position = avatar.position + avatar.velocity * deltaT; |
(Side note: this is actually a rough approximation - you can get a better, less framerate-dependent approximation with
avatar.position = avatar.position + (avatar.oldvelocity + avatar.velocity) * deltaT / 2.0f
, and you can get aclose-to-perfect simulation with something like this - but your players probably won't notice.)Constrain It
Your game probably has some system for colliding with world geometry. It probably looks something like this:
1 2 3 4 5 6 7 8 9 | Vector testPosition = avatar.position + avatar.velocity * deltaT; Vector intersection if ( RayCast( position, testPosition, out intersection )) { // we went through a wall, let's pull the character back, // using the normal of the wall // with a 1-unit space for breathing room testPosition = intersection + intersection.normal; } |
What happens to your avatar's velocity when they hit an obstacle? It doesn't make sense for your avatar to keep maintaining the same velocity through the wall. Some games might bounce you, using the normal of where you intersected to reflect your velocity; other games might let you slide along the wall; others will be somewhere in between. Let's look at code for sliding along a wall:
1 2 | avatar.velocity = (testPosition - avatar.position)/deltaT; avatar.position = testPosition; |
(If you're doing Verlet integration, where each frame you're already determining velocity simply by looking at previous position data, this step is already done for you.)
Also, videogames being the hacky things they are, you'll probably often find that your character's position snaps suddenly from one frame to the next in certain corner cases. When this happens, their velocity will go through the roof. My solution for that is simply a hack: check if their velocity gets too extreme and fix it if it does.
What does that code do when you hit a wall at an angle? The first frame, it changes your velocity dramatically as you hit the wall, but it's still pushing you through the wall. The next frame, your avatar gets updated into the wall again, and then pushed back out again, but now the velocity is the avatar's new position, slid along the wall, minus his old position, against the wall, so it's parallel to the wall.
This is a huge oversimplification of what goes on in the real world when one object collides with another, but most of your players won't notice or care.
At this point, though, we've successfully constrained our avatar's position and velocity with the walls and floors of our game. So now we're ready.
Constraining With Tethers Instead of Walls
So now imagine that your avatar is already attached by a virtual rope to a virtual point. As far as we're concerned, we can simply consider that an invisible circular or spherical wall. We can test collision by seeing if you've gotten too far away from the center of the circle, and pull the avatar back in.
1 2 3 4 5 6 7 8 9 | if ( amITethered ) { if (testPosition - tetherPoint ).Length() > tetherLength ) { // we're past the end of our rope // pull the avatar back in. testPosition = (testPosition - tetherPoint).Normalized() * tetherLength; } } |
Now, the same velocity adjustment that made us slide along walls will also make us slide along the inside of this virtual circle or sphere.
Some Subtlety and Nuance
At this point, you have a swinging game. But there are probably going to be a few things about it that get in the way of fun for your players, and this is where a little hacking can make things better.
Slack, Springiness
Depending on your game, and whether it is supposed to be simulating a length of rope or an elastic web or a grapple beam, you may have different approaches to slack and springiness. You can simulate a lot of stuff by tweaking the
tetherLength
. If you want to take slack out of your springy web or grapple beam, you can shorten the tetherLength
as the player gets closer to the tether point:1 | tetherLength = (avatar.position - tetherPoint).Length(); |
But if it's a nonelastic rope, you'd leave the
tetherLength
untouched.For springiness, you can have a
desiredLength
that's fixed and a currentLength
that continually tries to approach the desiredLength
- this will also be useful for our next step:But I Don't Want to Hit the Ground!
What if your avatar starts out in a low place and tries to swing? It's pretty obvious they won't get very far. A quick fix for that is to check how high above the ground the point they want to swing from is, and shorten the length of their tether so they'll clear the ground.
You can't just shorten it over one frame though, because then they'll suddenly snap into the air - so here, having a
desiredLength
that's short enough to not touch the ground and a currentLength
that rapidly approaches desiredLength
will get you the clearance you want.The Avatar's Up
If your avatar is a humanish figure, it doesn't make much sense for them to appear constantly perfectly vertical as they're swinging. Orienting them so they look like they're hanging from the rope - so they're upside down if they're doing a loop, for example - might look like this in Unity:
1 2 | Vector myUp = (avatar.position - tetherPoint); avatar.rotation = Quaternion.LookRotation( avatar.rotation.forward, myUp ); |
This will let the avatar swing backwards. You could also use the avatar's velocity for their forward (that's what I do) - or let them spin crazily...
Wrapping Around Things
In the real world, ropes wrap around stuff. A quick way to simulate that in your code is to raycast along the virtual rope each frame, and if it hits something, make a new attachment point where it intersects. This won't look right if the avatar then swings back on a nonsticky rope, but is fine for a sticky web, tongue or grapple beam.
Tweaking the way your rope wraps can have a big impact on fun. Suddenly wrapping around an outcropping can take the player by surprise and make them frustrated. We had a three step solution in Spider-Man 2: if you were too close to the outcropping, the web would break; if you were in a middle distance, the web would wrap; and if you were far away, the web would just go through.
Considerations for 3D
The hardest thing about bringing this 2D mechanic to 3D is considering the interface for the player - how they pick points in the world to swing from. Different games use different methods for picking such a point. Ratchet & Clank has fixed points in the world you can grapple; with the Quake grappling-hook mod and Bionic Commando: Rearmed you point the camera at what you want to attach to; Spider-Man 2 and Energy Hook cast rays out relative to the character, and where the rays intersect physical geometry that's the point where you attach.
Almost all of these methods involve raycasting against the world's physical geometry, whether it's along the line of the camera or from the character to the point of the mouse click - the intersection of the raycast determines your new tetherpoint.
For example, here's a mouse-look raycast that might be good for a first-person game:
1 2 3 4 5 6 7 | RaycastHit wallData; if ( Physics.Raycast( camera.position, camera.forward, out wallData, maximumTetherLength )) { amITethered = true ; tetherPoint = wallData.point; tetherLength = Vector3.Distance( wallData.point, avatar.position ); } |
Bumping Into Stuff
Bumping into walls and the like also tends to happen a lot more often in a 3D game than a 2D game, especially when it's the walls that you're tethering yourself to. There are a variety of things you can do to make this less annoying.
- Let the player steer in the air: physics doesn't have a lot to say about this but it somehow feels right. That's one of the things we did in Spider-Man 2, and why you get a jetpack in Energy Hook.
- Play a cool animation: have the avatar do some kind of flip or spin when they hit a wall, and then it feels less like a mistake and more like "I meant to do that!"
- Keep them off the wall: Ultimate Spider-Man did this - if you got too close to a wall it would gently push you away from it. It looked a little weird if you let go of the stick and just let your avatar hang there - they'd float away from the wall at a strange angle - but for the rest of the game it was an improvement. (You could possibly have the best of both worlds by only pushing away if the character is going a certain speed?)
- Reward them for not hitting the wall in the first place: This is the Energy Hook strategy - teach the player to avoid hitting walls, by rewarding them for clean swings. On the one hand, it encourages them to swing pretty; on the other hand, when they do hit the wall it's that much more frustrating, because they lose their style points.
So What Are You Waiting For?
Go on out and make your own swinging games, and let me know what you come up with! I can never get enough of swinging games.
No comments:
Post a Comment