I wanted to do at least a few posts though for an easy reference for something that I feel there is a dearth of information on for budding gamers trying to program in Unity. When I started to search for advice on how to wall run in Unity, I found just people asking questions on Unity Answers, and really they were too vague for the area. People appropriately posted, "Use a ray cast", and that was all well and good, but there was very little detail. Now that I've worked this stuff out on my own, I wanted to post a tutorial as a reference for people looking to do the same thing.
I'm not saying mine is best, or even optimized. For some projects, this could be a terrible idea for all I know. This all is posted here as a learning experience.
The goals of my project that got me started have been fairly simple:
- Have first-person perspective parkour movement
- Have procedural puzzles generated level by level so it's always different and ever-continuing
So my choices have all worked for this concept so far. If you want to spawn millions of enemies and have bullets flying everywhere like in Mirror's Edge, you may like to try to be very efficient with ray casts if my ideas don't help. This also means that by and large I haven't made this class modular enough to just be tossed onto an NPC and have it use this motor, but you could potentially do that with some modification.
Anyway, without further ado...
Basic Movement!
Before you can go running on walls, you need to be running on the ground. There's a stunning variety of tutorials for this too, and having looked through a lot of them, they can be confusing. I wanted standard, WASD movement, and my controls ended up like this:
- W - forward
- A - Backwards
- S - Strafe left
- D - Strafe Right
- Space - Jump
I'll guide you how to get there now.
Create the world
To start, create a new project, and make sure to import at the very least the character controllers that Unity comes with. This will save you some time later on.
I'm not going to go into detail for the following steps, you should probably be familiar in general, but you need to create the world. My standard run for this is:
- Create a cube (some just do a plane), scale its X and Z properties to 100 or something, make sure its position is 0,0,0. Now you have a platform.
- Create directional light (so you can see.) Point it in some direction, position doesn't matter. I usually put it at an angle so the shadows can help me discern what's going on.
- Create a capsule. Parent the main camera to it. The easiest way is to click on the main camera and drag it to the capsule in the heirarchy. Make sure the capsule's position is above your ground platform. Mine is at 6 meters above, so it actually starts with a drop.
Now you have a person! I actually went about this fairly backwards, since I was experimenting. I dragged the FPS controller prefab out from my project into the scene, and started deleting things off it in my inspector view (the instance I pulled out, not the prefab itself) to figure out what I needed and how to do what I was going to do.
The next thing you are going to want to do is grab your capsule, and go to "Add Component" in the Inspector window. Then just type "Character Controller", and it will bring you to an item that you want. It's going to have a little icon like a capsule with three smaller capsules around it. At least, it does in my setup.
The Character Controller is an important helper that allows you to deal with movement while also having collisions set up with a rigidbody. It does so much stuff for you that I'm telling you to use this thing, and I'm not even sure of the full-depth of its utility yet. If you are not familiar with it, check out the documentation for it.
Test your scene, make sure it doesn't crash. Make sure you can see stuff. Your camera won't move. Why? because we have nothing set up!
Now, the character controllers package has a lot of scripts in it that can give you a general FPS setup, but it's in Javascript, and I hate Javascript. It's also gargantuan, and a huge file is difficult to modify when you don't know what's going on (and I'm presuming if you're here, you don't want to wade through all that code.) We're going to start from brass tacks here.
Looking Around
We need to start on our first script! Create a new script somehow. You can click on your capsule in heirarchy, go to "Add Component", and then add a new C# script, or my preferred way is to go to my project's Assets folder in the project window, add a "Scripts" folder (my own, separate from the "StandardAssets\Scripts" folder, that way I know what's mine and what I've imported), then add a new C# file. You can name it however you want, but think of it is a Motor. I named mine "jumpMotor", because my project's working title is "Jump!" and I forgot that I prefer to PascalCase files, but you can name yours whatever. "myMotor" always works if you're just learning.
Now, I'm going to present all this like I'm some genius, but I went through a lot of research and experimentation, and an entire iterative process. Never feel bad if you're trying something new and it doesn't come together like a tutorial does.
Open your new C# file in MonoDevelop, or your preferred editor if you have a different one set up.
At the very beginning of your file, lets just do a little bit of housekeeping. You'll have your Using imports that come standard, and your class declaration below that. We want to add line that makes sure the Character Controller component is on the item you're about to create. Add this:
using UnityEngine;
using System.Collections;
[RequireComponent(typeof(CharacterController))]
public class jumpMotor : MonoBehaviour {
The "RequireComponent" sets it up so that the script will always make sure there's a CharacterController on the object it's placed on. If there isn't a CharacterController on the object there when the script is added, Unity will add one for you if "RequireComponent" is set up. If you try to accidentally delete the Character Controller, Unity will tell you that you can't.
Now, the next step is a little more housekeeping to get started since this is an FPS. We want to have a reference in our script to the controller since we'll be using it, and I like to cache a reference to the camera since we'll also be using this. You should probably just set a public camera variable and set the Main Camera to that variable in the Inspector. Unity gives me a warning (but not an error) over the code I'm about to show you, but I'm lazy and haven't changed it:
Private CharacterController controller;
Public Camera camera;
void Start(){
camera = Camera.main; // You'll get a warning over this, but it's fine for now.
controller = GetComponent<ChacterController>(); // We know this exists because of our "RequireComponent"
}
Now we have a good start (no pun intended)! This makes sure we can safely reference two items we need to move and look at things. From here, the real magic can start!
Our next step will be to just start looking around. We'll start doing this magic in our Update() function. For those who are very new, Update() is called basically every frame, and is the driving force behind this motor. I'm going to do the filthy, teacher-y thing where I have you put the code in the wrong place, then make you change it later. Start asking yourself why wouldn't you want this code here? But if you don't know where to put it now, just follow along, and update your Update() to this:
// Here are some quality of life variables so we can adjust movement a little more easily.
Public float TurnSpeed = 90f;
Public float MouseSensitivity = 2.5f;
float cameraRotX = 0f;
void Update() {
transform.Rotate (0f, (Input.GetAxis("Mouse X") * MouseSensitivity) * TurnSpeed * Time.deltaTime, 0f);
camera.transform.forward = transform.forward;
cameraRotX -= Input.GetAxis("Mouse Y") * MouseSensitivity;
camera.transform.Rotate(cameraRotX, 0f, 0f);
}
Now what we're doing here is setting up two variables to make our lives easier. TurnSpeed is how quickly you turn in general. I decided to mess around with MouseSensitivity as well, which means TurnSpeed can still be used for custom, scripted rotations and MouseSensitivity only comes in when doing mouse turns.
cameraRotX is different, that variable is to keep track of the rotation on the X-axis. I included this because if I updated it from 0 every time, I could never get the rotation I wanted.
What then happens is the capsule itself (of which the camera is a child) is rotated around its Y-axis based on the mouse's X position multiplied by the turnspeed, mouse sensitivity, and Time.deltaTime. This is the amount of time since the last frame in fractions of a second. If it's been half a second (hopefully not), Time.deltaTime would be 0.5. This means I turning is based on how fast things are being processed and rendered, which is a good thing. You'll see Time.deltaTime a lot if you're not familiar with it.
Then we update the camera's forward, so it's the same forward as the capsule. Trust me, you want to do this, it just keeps your camera movement clean. I recently tried to reorder this to do some fancy camera tricks, and things went insane.
cameraRotX is then updated with the change in the Mouse Y, and the camera's transform is changed again.
Now if you test your new Update() function, you should find yourself able to look around, but you don't move! Lets fix that.
I like to move it, move it!
So now we're looking around. We can aim ourselves. This is great! Now we just need to apply movement. We're still just experimenting, right? So now that we're faced in the right direction, lets add some code to the tail end of our update. We're also going to add some more helper stuff that will be very useful later on.
Public float TurnSpeed = 90f;
Public float MouseSensitivity = 2.5f;
Private float cameraRotX = 0f;
Private Vector3 moveDirection;
Public float BaseSpeed = 4.0f;
Public float JumpSpeed = 8.0f;
Public float Gravity = 20.0f;
void Update() {
transform.Rotate (0f, (Input.GetAxis("Mouse X") * MouseSensitivity) * TurnSpeed * Time.deltaTime, 0f);
camera.transform.forward = transform.forward;
cameraRotX -= Input.GetAxis("Mouse Y") * MouseSensitivity;
camera.transform.Rotate(cameraRotX, 0f, 0f);
// New code:
if (controller.isGrounded) {
moveDirection = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
moveDirection = transform.TransformDirection(moveDirection);
moveDirection.Normalize();
moveDirection *= BaseSpeed;
if (Input.GetButton("Jump")){
moveDirection.y = JumpSpeed;
}
}
moveDirection.y -= Gravity * Time.deltaTime;
controller.Move(moveDirection * Time.deltaTime);
}
Okay, now it may seem like a bit to digest, but don't worry. We've added a few new things. A moveDirection that's at the class level because we will be manipulating this a lot. This is a vector3, and handles walking forward and backwards, strafing, and jumping. JumpSpeed controls how high the motor jumps, BaseSpeed is how much movement is applied every frame, and Gravity is how quickly we fall. These are all public so you can manipulate them while testing out your code in the Unity editor.
Our first line of code makes our first reference to the controller to check if we're grounded. That is, the bottom of the capsule is colliding with ground. If it is, then we can do the rest of our movement. If it isn't, then the script will only apply gravity, dragging the capsule down until it hits something. This way you're not reversing direction in the middle of flight. If you want to change direction in the middle of flight, don't check for this, but for the project I'm making, once you are in the air, you are committed!
After that check, like I said, we update the movement. The moveDirection queries Unity's input, which is conveniently tracking our W,A,S,D keys for presses without having to say anything. This is what it's doing when it's doing GetAxisRaw. There's also regular GetAxis, but chose to work with the raw variables, "just in case" in my head. Choose whichever.
The move direction is transformed to be appropriate for the game object's local space, otherwise you'd turn around and backwards (S) would be forward, and forwards (W) would be backwards! That would be bad.
Finally the moveDirection is normalized, which means the vector keeps the same direction, but its length is 1. What that means is on the next line, where we multiply the whole vector times the base speed, it's like updating something that could move the range of 1 meter per second by 4. So now it's 4 meters per second in whatever direction we're facing, including diagonals! If you've ever programmed movement in a game, you know that moving faster on the diagonal is always a problem.
Following this, we check if we're jumping. This makes sense to do on the ground, because unless you have the mythical double-jump, you can't jump in the air. All we have to do is see if Input has detected "Jump", which defaults to space bar, and then apply the Jump speed to the moveDirections Y value. Note that we directly assign it, we don't add it. This is because moveDirection represents magnitude and direction, not position.
Now, the final steps, we subtract gravity from the movement using Gravity multiplied by deltaTime, so this will be a small amount, but over a second it will be equivalent to dropping by 20.
The very, very last thing we now do every frame is inform the controller what we're moving by. We've worked very hard to apply all movement we need to, so now we just call, controller.Move(), and pass it in our direction multiplied by the deltaTime. controller.Move() doesn't automatically account for frame time, so we have to multiply it on our own. Test it out!
I'm going to stop this blog here. It's already gone pretty long, and it is a lot to digest. For the next blog, I'll do some advanced movement with adding momentum to the forward run, so the user goes from the base speed to a full run speed as the user runs continuously. I may also start on the wall run, I'll at least start talking about the encapsulation that we want to really make this a powerful movement class.
Please let me know if there are any errors or stuff that doesn't work, as this is my first time writing a tutorial like this, and this more basic code was pulled from previous experience rather than a pre-typed, pre-tested class. The final code will be that, but not this code.
After that check, like I said, we update the movement. The moveDirection queries Unity's input, which is conveniently tracking our W,A,S,D keys for presses without having to say anything. This is what it's doing when it's doing GetAxisRaw. There's also regular GetAxis, but chose to work with the raw variables, "just in case" in my head. Choose whichever.
The move direction is transformed to be appropriate for the game object's local space, otherwise you'd turn around and backwards (S) would be forward, and forwards (W) would be backwards! That would be bad.
Finally the moveDirection is normalized, which means the vector keeps the same direction, but its length is 1. What that means is on the next line, where we multiply the whole vector times the base speed, it's like updating something that could move the range of 1 meter per second by 4. So now it's 4 meters per second in whatever direction we're facing, including diagonals! If you've ever programmed movement in a game, you know that moving faster on the diagonal is always a problem.
Following this, we check if we're jumping. This makes sense to do on the ground, because unless you have the mythical double-jump, you can't jump in the air. All we have to do is see if Input has detected "Jump", which defaults to space bar, and then apply the Jump speed to the moveDirections Y value. Note that we directly assign it, we don't add it. This is because moveDirection represents magnitude and direction, not position.
Now, the final steps, we subtract gravity from the movement using Gravity multiplied by deltaTime, so this will be a small amount, but over a second it will be equivalent to dropping by 20.
The very, very last thing we now do every frame is inform the controller what we're moving by. We've worked very hard to apply all movement we need to, so now we just call, controller.Move(), and pass it in our direction multiplied by the deltaTime. controller.Move() doesn't automatically account for frame time, so we have to multiply it on our own. Test it out!
I'm going to stop this blog here. It's already gone pretty long, and it is a lot to digest. For the next blog, I'll do some advanced movement with adding momentum to the forward run, so the user goes from the base speed to a full run speed as the user runs continuously. I may also start on the wall run, I'll at least start talking about the encapsulation that we want to really make this a powerful movement class.
Please let me know if there are any errors or stuff that doesn't work, as this is my first time writing a tutorial like this, and this more basic code was pulled from previous experience rather than a pre-typed, pre-tested class. The final code will be that, but not this code.
Awesome post! I really appreciate the tutorial, it's very well laid out, and it's clear you put a lot of effort into writing this all up. I've been searching for a parkour-game tutorial in unity for a long time now, thank you so much for your help! :)
ReplyDeletethx this helped
ReplyDelete