In this installment, we’ll look at how to make a first-person character. We’ll use the CSG-based level we designed in the previous part as a place to walk around and test our movement.
In an FPS or similar game, we want to give the player the illusion that they’re looking out of the character’s eyes. One nice aspect of this is that we don’t really need a model, at least to get started.
Start with a
KinematicBody. To this we’ll add two
CollisionShape nodes (“Body” and “Feet”). We also want to have a
Camera, but we need to be careful about how we handle rotation. The character should rotate in Y, but only the camera should rotate in X (for looking up and down). To make this work, add a
Spatial node, which we’ll call “Pivot”, and add the
Camera to that.
The “Body” collision shape is going to represent the player’s body. We can use a
90 degrees around X). Set its Radius to
0.5 and Height to
One issue with using a
CapsuleShape is that the bottom is rounded. This is good for moving over small bumps in a natural way, but it also means that when standing on the edge of a surface, the player will “roll” off in a strange way. We can prevent this by using a
BoxShape for the “Feet” collision. Make its extents
(.4, .1, .4) and place it so that its bottom is just above the bottom of the capsule. We’ll still have the round bottom to move over things, but the box will keep us from sliding off ledges.
Pivot up a bit, so that it’s not pointing the camera out of the “middle” of the body.
Much of the code for movement is the same as we used for the third-person character earlier in this series. We’ll start with declaring our variables:
extends KinematicBody onready var camera = $Pivot/Camera var gravity = -30 var max_speed = 8 var mouse_sensitivity = 0.002 # radians/pixel var velocity = Vector3()
The value of
mouse_sensitivity maps the movement of the mouse, which is in pixels, to rotation, in degrees. So for each pixel the mouse moves, we’ll rotate by
0.002 radians (or about
func get_input(): var input_dir = Vector3() # desired move in camera direction if Input.is_action_pressed("move_forward"): input_dir += -camera.global_transform.basis.z if Input.is_action_pressed("move_back"): input_dir += camera.global_transform.basis.z if Input.is_action_pressed("strafe_left"): input_dir += -camera.global_transform.basis.x if Input.is_action_pressed("strafe_right"): input_dir += camera.global_transform.basis.x input_dir = input_dir.normalized() return input_dir
When detecting input, we want to move in the direction the camera is facing, which is not necessarily the current movement direction. Although we’re not going to do it yet, this will allow us to implement friction and acceleration for a more natural movement feel.
We add up the inputs and return the resulting direction vector.
func _unhandled_input(event): if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: rotate_y(-event.relative.x * mouse_sensitivity) $Pivot.rotate_x(-event.relative.y * mouse_sensitivity) $Pivot.rotation.x = clamp($Pivot.rotation.x, -1.2, 1.2)
We also need to capture mouse movement for camera rotation. As discussed above, horizontal mouse movement rotates the entire body in Y, while vertical motion rotates the helper node in X.
We also need to limit that vertical rotation to prevent the camera from flipping upside-down.
func _physics_process(delta): velocity.y += gravity * delta var desired_velocity = get_input() * max_speed velocity.x = desired_velocity.x velocity.z = desired_velocity.z velocity = move_and_slide(velocity, Vector3.UP, true)
_physics_process() we get the
desired_velocity, the direction vector returned by
get_input(), and multiply it by
max_speed to set its length. We don’t want to alter our
velocity.y because that’s set by gravity, so we only set the
z components based on the input.
We’re also setting
true for the
stop_on_slope parameter of
move_and_slide(). This keeps us from sliding down the ramp if we stand on it.
Test your movement and ensure everything is working as expected.
First person characters typically have some sort of item, or at least empty hands, visible in front of them. We’re going to use the shotgun model from the following art pack:
MeshInstance to the
Pivot node and use the
shotgun.obj model from the art pack. You’ll notice that the model is too small for our player’s scale, so set Scale to
(8, 8, 8).
We need to position the model so that it’s projecting out “through” the camera. Aligning it can be difficult, so click the “View” menu and choose “2 Viewports”. In the bottom one, select the
Camera and click “Preview”, and in the top you can move the gun until it looks right.
We now have a basic first-person character controller. This could make a good foundation for a wide variety of game types by adding features and behaviors, as we’ll see in later lessons.
You can also find a video version of this lesson here: