Убийство игрока¶
Мы можем убивать врагов, прыгая на них, но игрок всё равно не может умереть. Давайте это исправим.
Мы хотим, чтобы определение попадания врага отличалось от его раздавливания. Мы хотим, чтобы игрок умирал, если он движется по полу, но не умирал, если он находится в воздухе. Мы могли бы использовать векторную математику для различия этих двух видов столкновений. Однако вместо этого мы воспользуемся узлом Area, который хорошо подходит для хитбоксов.
Хитбокс с помощью узла Area¶
Вернитесь к сцене Player и добавьте новый узел Area. Назовите его MobDetector. Добавьте узел CollisionShape в качестве его дочернего узла.
В инспекторе назначьте ему форму цилиндра.
Вот трюк, который можно использовать, чтобы столкновения происходили только тогда, когда игрок находится на земле или близко к ней. Вы можете уменьшить высоту цилиндра и переместить его в верхнюю часть персонажа. Таким образом, когда игрок прыгнет, форма окажется слишком высоко, чтобы враги могли столкнуться с ней.
Вы также хотите, чтобы цилиндр был шире сферы. Таким образом, игрок получит удар до столкновения и будет отброшен на верхнюю часть поля столкновения монстра.
Чем шире цилиндр, тем легче будет убить игрока.
Затем снова выберите узел MobDetector и в инспекторе отключите его свойство Monitorable. Это свойство сделает так, что другие физические узлы не смогут обнаружить эту область. Дополнительное свойство Monitoring позволяет ему обнаруживать столкновения. Затем удалите Collision -> Layer и установите маску на слой "enemies".
Когда области обнаруживают столкновение, они издают сигналы. Мы подключим один из них к узлу Player. На вкладке Node дважды щёлкните на сигнале body_entered
и подключите его к узлу Player.
MobDetector будет излучать body_entered
, когда в него попадает узел KinematicBody или RigidBody. Поскольку он маскирует только физические слои "enemies", он будет обнаруживать только узлы Mob.
В коде мы собираемся сделать две вещи: выдать сигнал, который мы позже используем для завершения игры, и уничтожить игрока. Мы можем обернуть эти операции в функцию die()
, которая поможет нам поместить описательную метку на код.
# Emitted when the player was hit by a mob.
# Put this at the top of the script.
signal hit
# And this function at the bottom.
func die():
emit_signal("hit")
queue_free()
func _on_MobDetector_body_entered(_body):
die()
// Don't forget to rebuild the project so the editor knows about the new signal.
// Emitted when the player was hit by a mob.
[Signal]
public delegate void Hit();
// ...
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
// We also specified this function name in PascalCase in the editor's connection window
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
Попробуйте запустить игру снова, нажав F5. Если всё настроено правильно, персонаж должен умереть, когда на него натолкнётся враг.
Однако, обратите внимание, что это полностью зависит от размера и положения фигур столкновения Player и Mob'. Вам может понадобиться переместить их и изменить их размер, чтобы добиться полноценного ощущения игры.
Завершение игры¶
Мы можем использовать сигнал Player hit
для завершения игры. Все, что нам нужно сделать, это подключить его к узлу Main и остановить MobTimer в ответ.
Откройте Main.tscn
, выберите узел Player, и в панели Node подключите его сигнал hit
к узлу Main.
Получение и остановка таймера в функции _on_Player_hit()
.
func _on_Player_hit():
$MobTimer.stop()
// We also specified this function name in PascalCase in the editor's connection window
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
Если вы попробуете игру сейчас, монстры перестанут появляться, когда вы умрёте, а оставшиеся покинут экран.
Вы можете похлопать себя по спине: вы создали прототип полноценной 3D-игры, даже если он еще немного не доработан.
Далее мы добавим счёт, возможность повторного прохождения игры, и вы увидите, как можно сделать игру более живой с помощью минималистичных анимаций.
Кодовая контрольная точка¶
Для справки, здесь приведены полные скрипты для узлов Main, Mob, и Player. Вы можете использовать их для сравнения и проверки своего кода.
Начиная с Main.gd
.
extends Node
export(PackedScene) var mob_scene
func _ready():
randomize()
func _on_MobTimer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instance()
# Choose a random location on the SpawnPath.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.unit_offset = randf()
# Communicate the spawn location and the player's location to the mob.
var player_position = $Player.transform.origin
mob.initialize(mob_spawn_location.translation, player_position)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
func _on_Player_hit():
$MobTimer.stop()
public class Main : Node
{
#pragma warning disable 649
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
}
public void OnMobTimerTimeout()
{
// Create a new instance of the Mob scene.
var mob = (Mob)MobScene.Instance();
// Choose a random location on the SpawnPath.
// We store the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.UnitOffset = GD.Randf();
// Communicate the spawn location and the player's location to the mob.
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
}
Далее Mob.gd
.
extends KinematicBody
# Emitted when the player jumped on the mob.
signal squashed
# Minimum speed of the mob in meters per second.
export var min_speed = 10
# Maximum speed of the mob in meters per second.
export var max_speed = 18
var velocity = Vector3.ZERO
func _physics_process(_delta):
move_and_slide(velocity)
func initialize(start_position, player_position):
look_at_from_position(start_position, player_position, Vector3.UP)
rotate_y(rand_range(-PI / 4, PI / 4))
var random_speed = rand_range(min_speed, max_speed)
velocity = Vector3.FORWARD * random_speed
velocity = velocity.rotated(Vector3.UP, rotation.y)
func squash():
emit_signal("squashed")
queue_free()
func _on_VisibilityNotifier_screen_exited():
queue_free()
public class Mob : KinematicBody
{
// Emitted when the played jumped on the mob.
[Signal]
public delegate void Squashed();
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
_velocity = Vector3.Forward * randomSpeed;
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
public void Squash()
{
EmitSignal(nameof(Squashed));
QueueFree();
}
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}
Наконец, самый длинный скрипт, Player.gd
.
extends KinematicBody
# Emitted when a mob hit the player.
signal hit
# 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
# Vertical impulse applied to the character upon jumping in meters per second.
export var jump_impulse = 20
# Vertical impulse applied to the character upon bouncing over a mob in meters per second.
export var bounce_impulse = 16
var 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(translation + direction, Vector3.UP)
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y += jump_impulse
velocity.y -= fall_acceleration * delta
velocity = move_and_slide(velocity, Vector3.UP)
for index in range(get_slide_count()):
var collision = get_slide_collision(index)
if collision.collider.is_in_group("mob"):
var mob = collision.collider
if Vector3.UP.dot(collision.normal) > 0.1:
mob.squash()
velocity.y = bounce_impulse
func die():
emit_signal("hit")
queue_free()
func _on_MobDetector_body_entered(_body):
die()
public class Player : KinematicBody
{
// Emitted when the player was hit by a mob.
[Signal]
public delegate void Hit();
// 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;
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse = 20;
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse = 16;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
var direction = Vector3.Zero;
if (Input.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("move_back"))
{
direction.z += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
}
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_velocity.y += JumpImpulse;
}
_velocity.y -= FallAcceleration * delta;
_velocity = MoveAndSlide(_velocity, Vector3.Up);
for (int index = 0; index < GetSlideCount(); index++)
{
KinematicCollision collision = GetSlideCollision(index);
if (collision.Collider is Mob mob && mob.IsInGroup("mob"))
{
if (Vector3.Up.Dot(collision.Normal) > 0.1f)
{
mob.Squash();
_velocity.y = BounceImpulse;
}
}
}
}
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
}
До встречи в следующем уроке, где вы добавите счёт и возможность повторной попытки.