Двигаем игрока с помощью кода
Пришло время для написания кода! Мы собираемся использовать действия ввода, который мы уже создали в предыдущих частях, чтобы персонаж двигался.
Примечание
Для этого проекта мы будем следовать правилам именования Godot.
GDScript: Классы (узлы) используют PascalCase, переменные и функции - snake_case, константы - ALL_CAPS (см. Руководство по стилю GDScript).
C#: Классы, экспортные переменные и методы используют PascalCase, приватные поля используют _camelCase, локальные переменные и параметры используют camelCase (См. Руководство по стилю C#). Будьте внимательны, чтобы точно набирать имена методов при подключении сигналов.
Нажмите правой кнопкой мыши на узел Player и выберите Attach Script, чтобы добавить к нему новый скрипт. Во всплывающем окне настройте Шаблон на Empty перед тем, как нажать кнопку Создать.

Давайте начнем со свойств класса. Мы собираемся определить скорость движения, ускорение свободного падения, представляющее гравитацию, и скорость, которую мы будем использовать для движений персонажа.
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
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 { get; set; } = 14;
// The downward acceleration when in the air, in meters per second squared.
[Export]
public int FallAcceleration { get; set; } = 75;
private Vector3 _targetVelocity = Vector3.Zero;
}
Это свойства, характерные для тела в движении. target_velocity - это 3D vector, совмещающий в себе скорость с направлением. Здесь мы определяем его, как свойство, потому что хотим обновлять и использовать повторно его значения.
Примечание
Значения довольно сильно отличаются от 2D-кода, потому что расстояние здесь измеряется в метрах. В то время как в 2D тысяча единиц (пикселей) может соответствовать только половине ширины вашего экрана, в 3D же это будет равняться километру.
А сейчас давайте напишем код движения. Мы начинаем с вычисления ввода вектора направления, используя глобальный объект Input в _physics_process().
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
public override void _PhysicsProcess(double 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 += 1.0f;
}
if (Input.IsActionPressed("move_left"))
{
direction.X -= 1.0f;
}
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 += 1.0f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.Z -= 1.0f;
}
}
Здесь мы собираемся сделать расчёт, используя виртуальную функцию _physics_process()``вместо ``_process(). Она создана специально для кода, связанного с физикой, например, кода движения кинематического или твёрдого тела. Она обновляет узел через фиксированные интервалы времени.
См. также
Для получения дополнительной информации о разнице между _process() и _physics_process(), прочитайте Idle and Physics Processing.
Сперва инициализируем переменную direction со значением Vector3.ZERO. После этого мы проверяем, использует ли игрок один или больше вводов move_* и обновляем x и z векторов. Они соответствуют осям плоскости земли.
Эти четыре условия дают нам восемь возможных вариантов и восемь возможных направлений.
В случае, если игрок нажмет, например, обе клавиши W и D одновременно, вектор будет иметь длину около 1.4. Но если он нажимает одну клавишу, то вектор будет иметь длину 1. Мы хотим, чтобы длина вектора была неизменной и чтобы игрок не двигался быстрей по диагонали. Чтобы сделать это, мы можем вызвать его метод normalize().
func _physics_process(delta):
#...
if direction != Vector3.ZERO:
direction = direction.normalized()
# Setting the basis property will affect the rotation of the node.
$Pivot.basis = Basis.looking_at(direction)
public override void _PhysicsProcess(double delta)
{
// ...
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
// Setting the basis property will affect the rotation of the node.
GetNode<Node3D>("Pivot").Basis = Basis.LookingAt(direction);
}
}
Здесь мы нормализуем вектор, только если направление имеет длину больше нуля, что означает, что игрок нажимает клавишу направления.
Мы вычисляем направление, в котором смотрит $Pivot, создавая Basis, который смотрит в направлении direction.
Затем мы обновляем скорость. Мы должны вычислить скорость на земле и скорость падения отдельно. Обязательно вернитесь на одну вкладку назад, чтобы строки находились внутри функции _physics_process(), но вне условия, которое мы только что написали.
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()
public override void _PhysicsProcess(double delta)
{
// ...
if (direction != Vector3.Zero)
{
// ...
}
// Ground velocity
_targetVelocity.X = direction.X * Speed;
_targetVelocity.Z = direction.Z * Speed;
// Vertical velocity
if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity
{
_targetVelocity.Y -= FallAcceleration * (float)delta;
}
// Moving the character
Velocity = _targetVelocity;
MoveAndSlide();
}
Метод CharacterBody3D.is_on_floor() возвращает true если тело столкнулось с землей. Именно поэтому мы применяем гравитацию только в ситуации, когда игрок находится в воздухе.
Для вертикальной скорости мы вычитаем ускорение свободного падения, умноженное на дельту времени, в каждом кадре. Эта строчка кода заставляет игрока падать каждый кадр, до тех пор пока он не столкнулся с землей.
Физический движок может только распознавать взаимодействия со стенами, полом или другими телами во время кадра, если происходит движение и столкновение. Позже мы будем использовать это свойство, чтобы написать код прыжка.
В последней строке мы вызываем CharacterBody3D.move_and_slide(). Это эффективный метод класса CharacterBody3D, который позволяет плавно передвигать персонажа. Если он ударяется об стену во время движения, то движок попытается сгладить действие. Он использует значение velocity, принадлежащее CharacterBody3D
Вот и весь код, необходимый для перемещения персонажа по полу.
Вот так выглядит завершенный код player.gd.
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()
# Setting the basis property will affect the rotation of the node.
$Pivot.basis = Basis.looking_at(direction)
# 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()
using Godot;
public partial class Player : CharacterBody3D
{
// How fast the player moves in meters per second.
[Export]
public int Speed { get; set; } = 14;
// The downward acceleration when in the air, in meters per second squared.
[Export]
public int FallAcceleration { get; set; } = 75;
private Vector3 _targetVelocity = Vector3.Zero;
public override void _PhysicsProcess(double delta)
{
var direction = Vector3.Zero;
if (Input.IsActionPressed("move_right"))
{
direction.X += 1.0f;
}
if (Input.IsActionPressed("move_left"))
{
direction.X -= 1.0f;
}
if (Input.IsActionPressed("move_back"))
{
direction.Z += 1.0f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.Z -= 1.0f;
}
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
// Setting the basis property will affect the rotation of the node.
GetNode<Node3D>("Pivot").Basis = Basis.LookingAt(direction);
}
// Ground velocity
_targetVelocity.X = direction.X * Speed;
_targetVelocity.Z = direction.Z * Speed;
// Vertical velocity
if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity
{
_targetVelocity.Y -= FallAcceleration * (float)delta;
}
// Moving the character
Velocity = _targetVelocity;
MoveAndSlide();
}
}
Проверка движения нашего игрока
Мы поместим нашего игрока в сцену Main, чтобы протестировать её. Чтобы это сделать, нам необходимо инстанцировать игрока и затем добавить камеру. В отличие от 2D, в 3D вы ничего не увидите, если в вашем окне просмотра нет камеры, указывающей на что-то.
Сохраните вашу сцену Player и откройте сцену Main. Вы можете нажать на вкладку Main вверху редактора и сделать это.

Если вы закрыли сцену до того, как сделать это, направьтесь во вкладку Файловая система и двойным нажатием щелкните на main.tscn, чтобы снова её открыть.
Для инстанцирования Player нажмите правой кнопкой мыши на узел Main и выберите Инстанцировать дочернюю сцену.

Во всплывающем окне двойным щелчком нажмите на player.tscn. Персонаж должен появиться в центре окна просмотра.
Добавление камеры
Теперь давайте добавим камеру. Точно так же, как мы сделали с Pivot принадлежащему Player, мы создадим основной каркас. Снова нажмите правой кнопкой мыши на узел Main и выберите в этот раз Добавить дочерний узел. Создайте новый Marker3D, назовите его CameraPivot и добавьте к нему дочерний узел Camera3D. Ваше дерево сцены должно выглядеть таким образом.

Обратите внимание на флажок Preview, который появляется в левом верхнем углу, при выбранной узле Camera. Вы можете нажать на него, чтобы просмотреть проекцию камеры в игре.

Мы будем использовать Pivot для поворота камеры, как будто она находится на кране. Сначала давайте разделим 3D-вид, чтобы иметь возможность свободно перемещаться по сцене и видеть то, что видит камера.
На панели инструментов прямо над окном просмотра нажмите Вид, затем 2 окна. Вы также можете нажать Ctrl + 2 (Cmd + 2 на macOS).


В нижнем окне выберите Camera3D и включите предварительный просмотр камеры, нажав на флажок.

На виде сверху, убедитесь, что выбрана Camera3D и переместите ее примерно на 19 единиц по оси Z (синяя стрелка).

Вот где происходит волшебство. Выберите CameraPivot и поверните его на 45 градусов вокруг оси X (с помощью красного круга). Вы увидите, что камера движется так, как будто она прикреплена к крану.

Вы можете запустить сцену, нажав F6 и нажимать клавиши со стрелками для перемещения персонажа.

Мы можем видеть пустое пространство вокруг персонажа из-за перспективы. В этой игре мы наоборот собираемся использовать орфографическую проекцию, чтобы лучше структурировать область геймплея и упростить для игрока считывание дистанций.
Снова выберите Camera и в инспекторе установите Projection на Orthogonal и Size на 19. Теперь персонаж должен выглядеть более плоским и поверхность должна заполнять задний план.
Примечание
При использовании ортогональной камеры в Godot 4 качество направленных теней зависит от значения Far камеры. Чем выше значение Far, тем дальше камера может видеть. Однако более высокие значения Far также снижают качество тени, так как рендеринг тени должен покрывать большее расстояние.
Если направленные тени выглядят слишком размытыми после переключения на ортогональную камеру, уменьшите свойство Far камеры до более низкого значения, например 100. Не уменьшайте свойство Far слишком сильно, иначе объекты вдали начнут исчезать.

Протестируйте вашу сцену: теперь вы должны иметь возможность двигаться во всех 8 направлениях и не проваливаться сквозь пол!
Таким образом, у нас есть и движение игрока, и вид. Далее мы будем работать над монстрами.