Files
godot-docs/getting_started/first_3d_game/03.player_movement_code.rst
2023-01-12 18:57:52 +01:00

430 lines
15 KiB
ReStructuredText

.. _doc_first_3d_game_player_movement:
Moving the player with code
===========================
It's time to code! We're going to use the input actions we created in the last
part to move the character.
Right-click the ``Player`` node and select *Attach Script* to add a new script to
it. In the popup, set the *Template* to *Empty* before pressing the *Create*
button.
|image0|
Let's start with the class's properties. We're going to define a movement speed,
a fall acceleration representing gravity, and a velocity we'll use to move the
character.
.. tabs::
.. code-tab:: gdscript GDScript
extends CharacterBody3D
# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration while in the air, in meters per second squared.
@export var fall_acceleration = 75
var target_velocity = Vector3.ZERO
.. code-tab:: csharp
using Godot;
public partial class Player : CharacterBody3D
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
// How fast the player moves in meters per second.
[Export]
public int Speed = 14;
// The downward acceleration while in the air, in meters per second squared.
[Export]
public int FallAcceleration = 75;
private Vector3 _velocity = Vector3.Zero;
}
These are common properties for a moving body. The ``velocity`` is a :ref:`3D vector <class_Vector3>`
combining a speed with a direction. Here, we define it as a property because
we want to update and reuse its value across frames.
.. note::
The values are quite different from 2D code because distances are in meters.
While in 2D, a thousand units (pixels) may only correspond to half of your
screen's width, in 3D, it's a kilometer.
Let's code the movement. We start by calculating the input direction vector
using the global ``Input`` object, in ``_physics_process()``.
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
# We create a local variable to store the input direction.
var direction = Vector3.ZERO
# We check for each move input and update the direction accordingly.
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
# Notice how we are working with the vector's x and z axes.
# In 3D, the XZ plane is the ground plane.
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// We create a local variable to store the input direction.
var direction = Vector3.Zero;
// We check for each move input and update the direction accordingly
if (Input.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("move_back"))
{
// Notice how we are working with the vector's x and z axes.
// In 3D, the XZ plane is the ground plane.
direction.z += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
}
Here, we're going to make all calculations using the ``_physics_process()``
virtual function. Like ``_process()``, it allows you to update the node every
frame, but it's designed specifically for physics-related code like moving a
kinematic or rigid body.
.. seealso::
To learn more about the difference between ``_process()`` and
``_physics_process()``, see :ref:`doc_idle_and_physics_processing`.
We start by initializing a ``direction`` variable to ``Vector3.ZERO``. Then, we
check if the player is pressing one or more of the ``move_*`` inputs and update
the vector's ``x`` and ``z`` components accordingly. These correspond to the
ground plane's axes.
These four conditions give us eight possibilities and eight possible directions.
In case the player presses, say, both W and D simultaneously, the vector will
have a length of about ``1.4``. But if they press a single key, it will have a
length of ``1``. We want the vector's length to be consistent, and not move faster diagonally. To do so, we can
call its ``normalize()`` method.
.. tabs::
.. code-tab:: gdscript GDScript
#func _physics_process(delta):
#...
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(position + direction, Vector3.UP)
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Node3D>("Pivot").LookAt(position + direction, Vector3.Up);
}
}
Here, we only normalize the vector if the direction has a length greater than
zero, which means the player is pressing a direction key.
In this case, we also get the ``Pivot`` node and call its ``look_at()`` method.
This method takes a position in space to look at in global coordinates and the
up direction. In this case, we can use the ``Vector3.UP`` constant.
.. note::
A node's local coordinates, like ``position``, are relative to their
parent. Global coordinates, like ``global_position`` are relative to the world's main axes you can see
in the viewport instead.
In 3D, the property that contains a node's position is ``position``. By
adding the ``direction`` to it, we get a position to look at that's one meter
away from the ``Player``.
Then, we update the velocity. We have to calculate the ground velocity and the
fall speed separately. Be sure to go back one tab so the lines are inside the
``_physics_process()`` function but outside the condition we just wrote above.
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
#...
if direction != Vector3.ZERO:
#...
# Ground Velocity
target_velocity.x = direction.x * speed
target_velocity.z = direction.z * speed
# Vertical Velocity
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
# Moving the Character
velocity = target_velocity
move_and_slide()
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
// Ground velocity
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Vertical velocity
_velocity.y -= FallAcceleration * delta;
// Moving the character
_velocity = MoveAndSlide(_velocity, Vector3.Up);
}
The ``CharacterBody3D.is_on_floor()`` function returns ``true`` if the body collided with the floor in this frame. That's why
we apply gravity to the ``Player`` only while he is in the air.
For the vertical velocity, we subtract the fall acceleration multiplied by the
delta time every frame.
This line of code will cause our character to fall in every frame, as long he is not on the floor, or collides with it.
The physics engine can only detect interactions with walls, the floor, or other
bodies during a given frame if movement and collisions happen. We will use this
property later to code the jump.
On the last line, we call ``CharacterBody3D.move_and_slide()`` which is a powerful
method of the ``CharacterBody3D`` class that allows you to move a character
smoothly. If it hits a wall midway through a motion, the engine will try to
smooth it out for you. It uses the *velocity* value native to the :ref:`CharacterBody3D <class_CharacterBody3D>`
.. OLD TEXT: The function takes two parameters: our velocity and the up direction. It moves
.. the character and returns a leftover velocity after applying collisions. When
.. hitting the floor or a wall, the function will reduce or reset the speed in that
.. direction from you. In our case, storing the function's returned value prevents
.. the character from accumulating vertical momentum, which could otherwise get so
.. big the character would move through the ground slab after a while.
And that's all the code you need to move the character on the floor.
Here is the complete ``Player.gd`` code for reference.
.. tabs::
.. code-tab:: gdscript GDScript
extends CharacterBody3D
# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration when in the air, in meters per second squared.
@export var fall_acceleration = 75
var target_velocity = Vector3.ZERO
func _physics_process(delta):
var direction = Vector3.ZERO
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(position + direction, Vector3.UP)
# Ground Velocity
target_velocity.x = direction.x * speed
target_velocity.z = direction.z * speed
# Vertical Velocity
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
# Moving the Character
velocity = target_velocity
move_and_slide()
.. code-tab:: csharp
using Godot;
public partial class Player : CharacterBody3D
{
// How fast the player moves in meters per second.
[Export]
public int Speed = 14;
// The downward acceleration when in the air, in meters per second squared.
[Export]
public int FallAcceleration = 75;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
// We create a local variable to store the input direction.
var direction = Vector3.Zero;
// We check for each move input and update the direction accordingly
if (Input.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("move_back"))
{
// Notice how we are working with the vector's x and z axes.
// In 3D, the XZ plane is the ground plane.
direction.z += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Node3D>("Pivot").LookAt(Translation + direction, Vector3.Up);
}
// Ground velocity
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Vertical velocity
_velocity.y -= FallAcceleration * delta;
// Moving the character
_velocity = MoveAndSlide(_velocity, Vector3.Up);
}
}
Testing our player's movement
-----------------------------
We're going to put our player in the ``Main`` scene to test it. To do so, we need
to instantiate the player and then add a camera. Unlike in 2D, in 3D, you won't
see anything if your viewport doesn't have a camera pointing at something.
Save your ``Player`` scene and open the ``Main`` scene. You can click on the *Main*
tab at the top of the editor to do so.
|image1|
If you closed the scene before, head to the *FileSystem* dock and double-click
``Main.tscn`` to re-open it.
To instantiate the ``Player``, right-click on the ``Main`` node and select *Instance
Child Scene*.
|image2|
In the popup, double-click ``Player.tscn``. The character should appear in the
center of the viewport.
Adding a camera
~~~~~~~~~~~~~~~
Let's add the camera next. Like we did with our *Player*\ 's *Pivot*, we're
going to create a basic rig. Right-click on the ``Main`` node again and select
*Add Child Node*. Create a new :ref:`Marker3D <class_Marker3D>`, and name it ``CameraPivot``. Select ``CameraPivot`` and add a child node :ref:`Camera3D <class_Camera3D>` to it. Your scene tree should look like this.
|image3|
Notice the *Preview* checkbox that appears in the top-left when you have the
*Camera* selected. You can click it to preview the in-game camera projection.
|image4|
We're going to use the *Pivot* to rotate the camera as if it was on a crane.
Let's first split the 3D view to be able to freely navigate the scene and see
what the camera sees.
In the toolbar right above the viewport, click on *View*, then *2 Viewports*.
You can also press :kbd:`Ctrl + 2` (:kbd:`Cmd + 2` on macOS).
|image11|
|image5|
On the bottom view, select your :ref:`Camera3D <class_Camera3D>` and turn on camera Preview by clicking
the checkbox.
|image6|
In the top view, move the camera about ``19`` units on the Z axis (the blue
one).
|image7|
Here's where the magic happens. Select the *CameraPivot* and rotate it ``-45``
degrees around the X axis (using the red circle). You'll see the camera move as
if it was attached to a crane.
|image8|
You can run the scene by pressing :kbd:`F6` and press the arrow keys to move the
character.
|image9|
We can see some empty space around the character due to the perspective
projection. In this game, we're going to use an orthographic projection instead
to better frame the gameplay area and make it easier for the player to read
distances.
Select the *Camera* again and in the *Inspector*, set the *Projection* to
*Orthogonal* and the *Size* to ``19``. The character should now look flatter and
the ground should fill the background.
|image10|
Test your scene and you should be able to move in all 8 directions and not glitch through the floor!
Ultimately, we have both player movement and the view in place. Next, we will
work on the monsters.
.. |image0| image:: img/03.player_movement_code/01.attach_script_to_player.webp
.. |image1| image:: img/03.player_movement_code/02.clicking_main_tab.png
.. |image2| image:: img/03.player_movement_code/03.instance_child_scene.png
.. |image3| image:: img/03.player_movement_code/04.scene_tree_with_camera.png
.. |image4| image:: img/03.player_movement_code/05.camera_preview_checkbox.png
.. |image5| image:: img/03.player_movement_code/06.two_viewports.png
.. |image6| image:: img/03.player_movement_code/07.camera_preview_checkbox.png
.. |image7| image:: img/03.player_movement_code/08.camera_moved.png
.. |image8| image:: img/03.player_movement_code/09.camera_rotated.png
.. |image9| image:: img/03.player_movement_code/10.camera_perspective.png
.. |image10| image:: img/03.player_movement_code/13.camera3d_values.webp
.. |image11| image:: img/03.player_movement_code/12.viewport_change.webp