|
@@ -9,14 +9,15 @@ Let's fix this.
|
|
|
We want to detect being hit by an enemy differently from squashing them.
|
|
|
We want the player to die when they're moving on the floor, but not if
|
|
|
they're in the air. We could use vector math to distinguish the two
|
|
|
-kinds of collisions. Instead, though, we will use an *Area* node, which
|
|
|
+kinds of collisions. Instead, though, we will use an :ref:`Area3D <class_Area3D>` node, which
|
|
|
works well for hitboxes.
|
|
|
|
|
|
Hitbox with the Area node
|
|
|
-------------------------
|
|
|
|
|
|
-Head back to the *Player* scene and add a new *Area* node. Name it
|
|
|
-*MobDetector*. Add a *CollisionShape* node as a child of it.
|
|
|
+Head back to the ``Player.tscn`` scene and add a new child node :ref:`Area3D <class_Area3D>`. Name it
|
|
|
+``MobDetector``
|
|
|
+Add a :ref:`CollisionShape3D <class_CollisionShape3D>` node as a child of it.
|
|
|
|
|
|
|image0|
|
|
|
|
|
@@ -38,8 +39,8 @@ monster's collision box.
|
|
|
|
|
|
The wider the cylinder, the more easily the player will get killed.
|
|
|
|
|
|
-Next, select the *MobDetector* node again, and in the *Inspector*, turn
|
|
|
-off its *Monitorable* property. This makes it so other physics nodes
|
|
|
+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.
|
|
@@ -47,14 +48,14 @@ the mask to the "enemies" layer.
|
|
|
|image3|
|
|
|
|
|
|
When areas detect a collision, they emit signals. We're going to connect
|
|
|
-one to the *Player* node. In the *Node* tab, double-click the
|
|
|
-``body_entered`` signal and connect it to the *Player*.
|
|
|
+one to the ``Player`` node. Select ``MobDetector`` and go to *Inspector*'s *Node* tab, double-click the
|
|
|
+``body_entered`` signal and connect it to the ``Player``
|
|
|
|
|
|
|image4|
|
|
|
|
|
|
-The *MobDetector* will emit ``body_entered`` when a *CharacterBody3D* or a
|
|
|
-*RigidBody* node enters it. As it only masks the "enemies" physics
|
|
|
-layers, it will only detect the *Mob* nodes.
|
|
|
+The *MobDetector* will emit ``body_entered`` when a :ref:`CharacterBody3D <class_CharacterBody3D>` or a
|
|
|
+:ref:`RigidBody3D <class_RigidBody3D>` node enters it. As it only masks the "enemies" physics
|
|
|
+layers, it will only detect the ``Mob`` nodes.
|
|
|
|
|
|
Code-wise, we're going to do two things: emit a signal we'll later use
|
|
|
to end the game and destroy the player. We can wrap these operations in
|
|
@@ -74,7 +75,7 @@ a ``die()`` function that helps us put a descriptive label on the code.
|
|
|
queue_free()
|
|
|
|
|
|
|
|
|
- func _on_MobDetector_body_entered(_body):
|
|
|
+ func _on_mob_detector_body_entered(_body):
|
|
|
die()
|
|
|
|
|
|
.. code-tab:: csharp
|
|
@@ -100,30 +101,37 @@ a ``die()`` function that helps us put a descriptive label on the code.
|
|
|
}
|
|
|
|
|
|
Try the game again by pressing :kbd:`F5`. If everything is set up correctly,
|
|
|
-the character should die when an enemy runs into it.
|
|
|
+the character should die when an enemy runs into the collider. Note that without a ``Player``, the following line
|
|
|
|
|
|
-However, note that this depends entirely on the size and position of the
|
|
|
-*Player* and the *Mob*\ 's collision shapes. You may need to move them
|
|
|
+.. tabs::
|
|
|
+ .. code-tab:: gdscript GDScript
|
|
|
+
|
|
|
+ var player_position = $Player.transform.origin
|
|
|
+
|
|
|
+gives error because there is no $Player!
|
|
|
+
|
|
|
+Also note that the enemy colliding with the player and dying depends on the size and position of the
|
|
|
+``Player`` and the ``Mob``\ 's collision shapes. You may need to move them
|
|
|
and resize them to achieve a tight game feel.
|
|
|
|
|
|
Ending the game
|
|
|
---------------
|
|
|
|
|
|
-We can use the *Player*\ 's ``hit`` signal to end the game. All we need
|
|
|
-to do is connect it to the *Main* node and stop the *MobTimer* in
|
|
|
+We can use the ``Player``\ 's ``hit`` signal to end the game. All we need
|
|
|
+to do is connect it to the ``Main`` node and stop the ``MobTimer`` in
|
|
|
reaction.
|
|
|
|
|
|
-Open ``Main.tscn``, select the *Player* node, and in the *Node* dock,
|
|
|
-connect its ``hit`` signal to the *Main* node.
|
|
|
+Open ``Main.tscn``, select the ``Player`` node, and in the *Node* dock,
|
|
|
+connect its ``hit`` signal to the ``Main`` node.
|
|
|
|
|
|
|image5|
|
|
|
|
|
|
-Get and stop the timer in the ``_on_Player_hit()`` function.
|
|
|
+Get the timer, and stop it, in the ``_on_player_hit()`` function.
|
|
|
|
|
|
.. tabs::
|
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
|
|
- func _on_Player_hit():
|
|
|
+ func _on_player_hit():
|
|
|
$MobTimer.stop()
|
|
|
|
|
|
.. code-tab:: csharp
|
|
@@ -147,7 +155,7 @@ animations.
|
|
|
Code checkpoint
|
|
|
---------------
|
|
|
|
|
|
-Here are the complete scripts for the *Main*, *Mob*, and *Player* nodes,
|
|
|
+Here are the complete scripts for the ``Main``, ``Mob``, and ``Player`` nodes,
|
|
|
for reference. You can use them to compare and check your code.
|
|
|
|
|
|
Starting with ``Main.gd``.
|
|
@@ -155,35 +163,32 @@ Starting with ``Main.gd``.
|
|
|
.. tabs::
|
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
|
|
- extends Node
|
|
|
-
|
|
|
- export(PackedScene) var mob_scene
|
|
|
+ extends Node
|
|
|
|
|
|
+ @export var mob_scene: PackedScene
|
|
|
|
|
|
- func _ready():
|
|
|
- randomize()
|
|
|
+ func _ready():
|
|
|
+ randomize()
|
|
|
|
|
|
|
|
|
- func _on_MobTimer_timeout():
|
|
|
- # Create a new instance of the Mob scene.
|
|
|
- var mob = mob_scene.instantiate()
|
|
|
+ func _on_mob_timer_timeout():
|
|
|
+ # Create a new instance of the Mob scene.
|
|
|
+ var mob = mob_scene.instantiate()
|
|
|
|
|
|
- # 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()
|
|
|
+ # Choose a random location on the SpawnPath.
|
|
|
+ # We store the reference to the SpawnLocation node.
|
|
|
+ var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
|
|
|
+ # And give it a random offset.
|
|
|
+ mob_spawn_location.progress_ratio = 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)
|
|
|
+ var player_position = $Player.position
|
|
|
+ mob.initialize(mob_spawn_location.position, player_position)
|
|
|
|
|
|
- # Spawn the mob by adding it to the Main scene.
|
|
|
- add_child(mob)
|
|
|
-
|
|
|
-
|
|
|
- func _on_Player_hit():
|
|
|
- $MobTimer.stop()
|
|
|
+ # Spawn the mob by adding it to the Main scene.
|
|
|
+ add_child(mob)
|
|
|
|
|
|
+ func _on_player_hit():
|
|
|
+ $MobTimer.stop()
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
public class Main : Node
|
|
@@ -210,7 +215,7 @@ Starting with ``Main.gd``.
|
|
|
mobSpawnLocation.UnitOffset = GD.Randf();
|
|
|
|
|
|
// Communicate the spawn location and the player's location to the mob.
|
|
|
- Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
|
|
|
+ Vector3 playerPosition = GetNode<Player>("Player").position;
|
|
|
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
|
|
|
|
|
|
// Spawn the mob by adding it to the Main scene.
|
|
@@ -228,40 +233,42 @@ Next is ``Mob.gd``.
|
|
|
.. tabs::
|
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
|
|
- extends CharacterBody3D
|
|
|
-
|
|
|
- # Emitted when the player jumped on the mob.
|
|
|
- signal squashed
|
|
|
+ extends CharacterBody3D
|
|
|
|
|
|
- # 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
|
|
|
+ # 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
|
|
|
+ # Emitted when the player jumped on the mob
|
|
|
+ signal squashed
|
|
|
|
|
|
+ func _physics_process(_delta):
|
|
|
+ move_and_slide()
|
|
|
|
|
|
- func _physics_process(_delta):
|
|
|
- move_and_slide(velocity)
|
|
|
+ # This function will be called from the Main scene.
|
|
|
+ func initialize(start_position, player_position):
|
|
|
+ # We position the mob by placing it at start_position
|
|
|
+ # and rotate it towards player_position, so it looks at the player.
|
|
|
+ look_at_from_position(start_position, player_position, Vector3.UP)
|
|
|
+ # In this rotation^, the mob will move directly towards the player
|
|
|
+ # so we rotate it randomly within range of -90 and +90 degrees.
|
|
|
+ rotate_y(randf_range(-PI / 4, PI / 4))
|
|
|
|
|
|
+ # We calculate a random speed (integer)
|
|
|
+ var random_speed = randi_range(min_speed, max_speed)
|
|
|
+ # We calculate a forward velocity that represents the speed.
|
|
|
+ velocity = Vector3.FORWARD * random_speed
|
|
|
+ # We then rotate the velocity vector based on the mob's Y rotation
|
|
|
+ # in order to move in the direction the mob is looking.
|
|
|
+ velocity = velocity.rotated(Vector3.UP, rotation.y)
|
|
|
|
|
|
- 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 _on_visible_on_screen_notifier_3d_screen_exited():
|
|
|
+ queue_free()
|
|
|
|
|
|
func squash():
|
|
|
- emit_signal("squashed")
|
|
|
- queue_free()
|
|
|
-
|
|
|
-
|
|
|
- func _on_VisibilityNotifier_screen_exited():
|
|
|
- queue_free()
|
|
|
-
|
|
|
+ emit_signal("squashed")
|
|
|
+ queue_free() # Destroy this node
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
public class Mob : CharacterBody3D
|
|
@@ -306,71 +313,90 @@ Next is ``Mob.gd``.
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-Finally, the longest script, ``Player.gd``.
|
|
|
+Finally, the longest script, ``Player.gd``:
|
|
|
|
|
|
.. tabs::
|
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
|
|
extends CharacterBody3D
|
|
|
|
|
|
- # 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()
|
|
|
-
|
|
|
+ signal hit
|
|
|
+
|
|
|
+ # 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
|
|
|
+ @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 target_velocity = Vector3.ZERO
|
|
|
+
|
|
|
+
|
|
|
+ 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 = direction.x + 1
|
|
|
+ if Input.is_action_pressed("move_left"):
|
|
|
+ direction.x = 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 = direction.z + 1
|
|
|
+ if Input.is_action_pressed("move_forward"):
|
|
|
+ direction.z = direction.z - 1
|
|
|
+
|
|
|
+ # Prevent diagonal moving fast af
|
|
|
+ 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
|
|
|
+ target_velocity.y = target_velocity.y - (fall_acceleration * delta)
|
|
|
+
|
|
|
+ # Jumping.
|
|
|
+ if is_on_floor() and Input.is_action_just_pressed("jump"):
|
|
|
+ target_velocity.y = jump_impulse
|
|
|
+
|
|
|
+ # Iterate through all collisions that occurred this frame
|
|
|
+ # in C this would be for(int i = 0; i < collisions.Count; i++)
|
|
|
+ for index in range(get_slide_collision_count()):
|
|
|
+ # We get one of the collisions with the player
|
|
|
+ var collision = get_slide_collision(index)
|
|
|
+
|
|
|
+ # If the collision is with ground
|
|
|
+ if (collision.get_collider() == null):
|
|
|
+ continue
|
|
|
+
|
|
|
+ # If the collider is with a mob
|
|
|
+ if collision.get_collider().is_in_group("mob"):
|
|
|
+ var mob = collision.get_collider()
|
|
|
+ # we check that we are hitting it from above.
|
|
|
+ if Vector3.UP.dot(collision.get_normal()) > 0.1:
|
|
|
+ # If so, we squash it and bounce.
|
|
|
+ mob.squash()
|
|
|
+ target_velocity.y = bounce_impulse
|
|
|
+
|
|
|
+ # Moving the Character
|
|
|
+ velocity = target_velocity
|
|
|
+ move_and_slide()
|
|
|
+
|
|
|
+ # And this function at the bottom.
|
|
|
+ func die():
|
|
|
+ emit_signal("hit")
|
|
|
+ queue_free()
|
|
|
+
|
|
|
+ func _on_mob_detector_body_entered(body):
|
|
|
+ die()
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
public class Player : CharacterBody3D
|
|
@@ -464,6 +490,6 @@ See you in the next lesson to add the score and the retry option.
|
|
|
.. |image0| image:: img/07.killing_player/01.adding_area_node.png
|
|
|
.. |image1| image:: img/07.killing_player/02.cylinder_shape.png
|
|
|
.. |image2| image:: img/07.killing_player/03.cylinder_in_editor.png
|
|
|
-.. |image3| image:: img/07.killing_player/04.mob_detector_properties.png
|
|
|
+.. |image3| image:: img/07.killing_player/04.mob_detector_properties.webp
|
|
|
.. |image4| image:: img/07.killing_player/05.body_entered_signal.png
|
|
|
.. |image5| image:: img/07.killing_player/06.player_hit_signal.png
|