Убивство гравця¶
Ми можемо убивати ворогів просто стрибнувши на них, проте гравець залишається безсмертним. Давайте пофіксимо це.
Успішний удар має надходити від ворога, який не підлягає розчавленню. Ми хочемо, щоб гравець помирав при зіткненні з монстрами на підлозі, але не коли знаходиться в повітрі. Ми могли б використовувати векторну математику, щоб розрізняти два типи зіткнень. Замість цього, однак, ми будемо використовувати вузол Area, який добре підходить для хітбоксів (hit - удар, boxes - коробки).
Хітбокс з вузла Area¶
Поверніться до сцени гравця Player та додайте новий вузол Area. Назвіть його MobDetector. Додайте вузол CollisionShape в якості нащадка.
В Інспекторі призначте йому форму циліндра.
Ось трюк, який ви можете використовувати, щоб відбирати тільки ті зіткнення, що коли гравець знаходиться на землі, або близько до неї. Ви можете зменшити висоту циліндра і перемістити його до верхньої частини персонажа. Таким чином, коли гравець стрибає, форма буде занадто високо, щоб вороги зіткнулися з нею.
Окрім цього циліндр має бути ширше сфери. Таким чином, гравець буде отримувати удар до того як сам нанесе його монстру.
Чим ширше циліндр, тим легше буде убивати гравця.
Next, select the MobDetector node again, and in the Inspector, turn off its Monitorable property. This makes it so other physics nodes cannot detect the area. The complementary Monitoring property allows it to detect collisions. Then, remove the Collision -> Layer and set the mask to the "enemies" layer.
Коли області (вузли Аrea) виявляють зіткнення, вони випромінюють сигнали. Ми збираємося підключити один до вузла гравця Player. На панелі Вузол двічі клацніть сигнал 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. Якщо все налаштовано правильно, персонаж повинен загинути при зіткненні з ворогом.
Однак запримітьте, що це повністю залежить від розміру та положення гравця та форм зіткнення Mob. Можливо, вам доведеться перемістити їх і змінити розмір, щоб досягти жорсткого ігрового відчуття.
Завершення гри¶
Ми можемо використовувати сигнал гравця Player hit
, для завершення гри. Все, що нам потрібно зробити, це підключити його до головного вузла Main і запрограмувати зупинку MobTimer при його надходженні.
Відкрийте Main.tscn
, виберіть вузол Player, а на панелі Вузол підключіть його сигнал 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();
}
}
До зустрічі на наступному уроці, щоб додати рахунок та можливість повторити спробу.