123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- .. _doc_first_3d_game_jumping_and_squashing_monsters:
- Jumping and squashing monsters
- ==============================
- In this part, we'll add the ability to jump and squash the monsters. In the next
- lesson, we'll make the player die when a monster hits them on the ground.
- First, we have to change a few settings related to physics interactions. Enter
- the world of :ref:`physics layers
- <doc_physics_introduction_collision_layers_and_masks>`.
- Controlling physics interactions
- --------------------------------
- Physics bodies have access to two complementary properties: layers and masks.
- Layers define on which physics layer(s) an object is.
- Masks control the layers that a body will listen to and detect. This affects
- collision detection. When you want two bodies to interact, you need at least one
- to have a mask corresponding to the other.
- If that's confusing, don't worry, we'll see three examples in a second.
- The important point is that you can use layers and masks to filter physics
- interactions, control performance, and remove the need for extra conditions in
- your code.
- By default, all physics bodies and areas are set to both layer and mask ``1``.
- This means they all collide with each other.
- Physics layers are represented by numbers, but we can give them names to keep
- track of what's what.
- Setting layer names
- ~~~~~~~~~~~~~~~~~~~
- Let's give our physics layers a name. Go to *Project -> Project Settings*.
- |image0|
- In the left menu, navigate down to *Layer Names -> 3D Physics*. You can see a
- list of layers with a field next to each of them on the right. You can set their
- names there. Name the first three layers *player*, *enemies*, and *world*,
- respectively.
- |image1|
- Now, we can assign them to our physics nodes.
- Assigning layers and masks
- ~~~~~~~~~~~~~~~~~~~~~~~~~~
- In the *Main* scene, select the ``Ground`` node. In the *Inspector*, expand the
- *Collision* section. There, you can see the node's layers and masks as a grid of
- buttons.
- |image2|
- The ground is part of the world, so we want it to be part of the third layer.
- Click the lit button to toggle **off** the first *Layer* and toggle **on** the third
- one. Then, toggle **off** the *Mask* by clicking on it.
- |image3|
- As mentioned before, the *Mask* property allows a node to listen to interaction
- with other physics objects, but we don't need it to have collisions. ``Ground`` doesn't need to listen to anything; it's just there to prevent
- creatures from falling.
- Note that you can click the "..." button on the right side of the properties to
- see a list of named checkboxes.
- |image4|
- Next up are the ``Player`` and the ``Mob``. Open ``player.tscn`` by double-clicking
- the file in the *FileSystem* dock.
- Select the *Player* node and set its *Collision -> Mask* to both "enemies" and
- "world". You can leave the default *Layer* property as it is, because the first layer is the
- "player" layer.
- |image5|
- Then, open the *Mob* scene by double-clicking on ``mob.tscn`` and select the
- ``Mob`` node.
- Set its *Collision -> Layer* to "enemies" and unset its *Collision -> Mask*,
- leaving the mask empty.
- |image6|
- These settings mean the monsters will move through one another. If you want the
- monsters to collide with and slide against each other, turn **on** the "enemies"
- mask.
- .. note::
- The mobs don't need to mask the "world" layer because they only move
- on the XZ plane. We don't apply any gravity to them by design.
- Jumping
- -------
- The jumping mechanic itself requires only two lines of code. Open the *Player*
- script. We need a value to control the jump's strength and update
- ``_physics_process()`` to code the jump.
- After the line that defines ``fall_acceleration``, at the top of the script, add
- the ``jump_impulse``.
- .. tabs::
- .. code-tab:: gdscript GDScript
- #...
- # Vertical impulse applied to the character upon jumping in meters per second.
- @export var jump_impulse = 20
- .. code-tab:: csharp
- // Don't forget to rebuild the project so the editor knows about the new export variable.
- // ...
- // Vertical impulse applied to the character upon jumping in meters per second.
- [Export]
- public int JumpImpulse { get; set; } = 20;
- Inside ``_physics_process()``, add the following code before the ``move_and_slide()`` codeblock.
- .. tabs::
- .. code-tab:: gdscript GDScript
- func _physics_process(delta):
- #...
- # Jumping.
- if is_on_floor() and Input.is_action_just_pressed("jump"):
- target_velocity.y = jump_impulse
- #...
- .. code-tab:: csharp
- public override void _PhysicsProcess(double delta)
- {
- // ...
- // Jumping.
- if (IsOnFloor() && Input.IsActionJustPressed("jump"))
- {
- _targetVelocity.Y = JumpImpulse;
- }
- // ...
- }
- That's all you need to jump!
- The ``is_on_floor()`` method is a tool from the ``CharacterBody3D`` class. It
- returns ``true`` if the body collided with the floor in this frame. That's why
- we apply gravity to the *Player*: so we collide with the floor instead of
- floating over it like the monsters.
- If the character is on the floor and the player presses "jump", we instantly
- give them a lot of vertical speed. In games, you really want controls to be
- responsive and giving instant speed boosts like these, while unrealistic, feels
- great.
- Notice that the Y axis is positive upwards. That's unlike 2D, where the Y axis
- is positive downwards.
- Squashing monsters
- ------------------
- Let's add the squash mechanic next. We're going to make the character bounce
- over monsters and kill them at the same time.
- We need to detect collisions with a monster and to differentiate them from
- collisions with the floor. To do so, we can use Godot's :ref:`group
- <doc_groups>` tagging feature.
- Open the scene ``mob.tscn`` again and select the *Mob* node. Go to the *Node*
- dock on the right to see a list of signals. The *Node* dock has two tabs:
- *Signals*, which you've already used, and *Groups*, which allows you to assign
- tags to nodes.
- Click on it to reveal a field where you can write a tag name. Enter "mob" in the
- field and click the *Add* button.
- |image7|
- An icon appears in the *Scene* dock to indicate the node is part of at least one
- group.
- |image8|
- We can now use the group from the code to distinguish collisions with monsters
- from collisions with the floor.
- Coding the squash mechanic
- ~~~~~~~~~~~~~~~~~~~~~~~~~~
- Head back to the *Player* script to code the squash and bounce.
- At the top of the script, we need another property, ``bounce_impulse``. When
- squashing an enemy, we don't necessarily want the character to go as high up as
- when jumping.
- .. tabs::
- .. code-tab:: gdscript GDScript
- # Vertical impulse applied to the character upon bouncing over a mob in
- # meters per second.
- @export var bounce_impulse = 16
- .. code-tab:: csharp
- // Don't forget to rebuild the project so the editor knows about the new export variable.
- // Vertical impulse applied to the character upon bouncing over a mob in meters per second.
- [Export]
- public int BounceImpulse { get; set; } = 16;
- Then, after the **Jumping** codeblock we added above in ``_physics_process()``, add the following loop. With
- ``move_and_slide()``, Godot makes the body move sometimes multiple times in a
- row to smooth out the character's motion. So we have to loop over all collisions
- that may have happened.
- In every iteration of the loop, we check if we landed on a mob. If so, we kill
- it and bounce.
- With this code, if no collisions occurred on a given frame, the loop won't run.
- .. tabs::
- .. code-tab:: gdscript GDScript
- func _physics_process(delta):
- #...
- # Iterate through all collisions that occurred this frame
- for index in range(get_slide_collision_count()):
- # We get one of the collisions with the player
- var collision = get_slide_collision(index)
- # If there are duplicate collisions with a mob in a single frame
- # the mob will be deleted after the first collision, and a second call to
- # get_collider will return null, leading to a null pointer when calling
- # collision.get_collider().is_in_group("mob").
- # This block of code prevents processing duplicate collisions.
- 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
- # Prevent further duplicate calls.
- break
- .. code-tab:: csharp
- public override void _PhysicsProcess(double delta)
- {
- // ...
- // Iterate through all collisions that occurred this frame.
- for (int index = 0; index < GetSlideCollisionCount(); index++)
- {
- // We get one of the collisions with the player.
- KinematicCollision3D collision = GetSlideCollision(index);
- // If the collision is with a mob.
- // With C# we leverage typing and pattern-matching
- // instead of checking for the group we created.
- if (collision.GetCollider() is Mob mob)
- {
- // We check that we are hitting it from above.
- if (Vector3.Up.Dot(collision.GetNormal()) > 0.1f)
- {
- // If so, we squash it and bounce.
- mob.Squash();
- _targetVelocity.Y = BounceImpulse;
- // Prevent further duplicate calls.
- break;
- }
- }
- }
- }
- That's a lot of new functions. Here's some more information about them.
- The functions ``get_slide_collision_count()`` and ``get_slide_collision()`` both come from
- the :ref:`CharacterBody3D <class_CharacterBody3D>` class and are related to
- ``move_and_slide()``.
- ``get_slide_collision()`` returns a
- :ref:`KinematicCollision3D<class_KinematicCollision3D>` object that holds
- information about where and how the collision occurred. For example, we use its
- ``get_collider`` property to check if we collided with a "mob" by calling
- ``is_in_group()`` on it: ``collision.get_collider().is_in_group("mob")``.
- .. note::
- The method ``is_in_group()`` is available on every :ref:`Node<class_Node>`.
- To check that we are landing on the monster, we use the vector dot product:
- ``Vector3.UP.dot(collision.get_normal()) > 0.1``. The collision normal is a 3D vector
- that is perpendicular to the plane where the collision occurred. The dot product
- allows us to compare it to the up direction.
- With dot products, when the result is greater than ``0``, the two vectors are at
- an angle of fewer than 90 degrees. A value higher than ``0.1`` tells us that we
- are roughly above the monster.
- After handling the squash and bounce logic, we terminate the loop early via the ``break`` statement
- to prevent further duplicate calls to ``mob.squash()``, which may otherwise result in unintended bugs
- such as counting the score multiple times for one kill.
- We are calling one undefined function, ``mob.squash()``, so we have to add it to
- the Mob class.
- Open the script ``mob.gd`` by double-clicking on it in the *FileSystem* dock. At
- the top of the script, we want to define a new signal named ``squashed``. And at
- the bottom, you can add the squash function, where we emit the signal and
- destroy the mob.
- .. tabs::
- .. code-tab:: gdscript GDScript
- # Emitted when the player jumped on the mob.
- signal squashed
- # ...
- func squash():
- squashed.emit()
- queue_free()
- .. code-tab:: csharp
- // Don't forget to rebuild the project so the editor knows about the new signal.
- // Emitted when the player jumped on the mob.
- [Signal]
- public delegate void SquashedEventHandler();
- // ...
- public void Squash()
- {
- EmitSignal(SignalName.Squashed);
- QueueFree();
- }
- .. note::
- When using C#, Godot will create the appropriate events automatically for all Signals ending with `EventHandler`, see :ref:`C# Signals <doc_c_sharp_signals>`.
- We will use the signal to add points to the score in the next lesson.
- With that, you should be able to kill monsters by jumping on them. You can press
- :kbd:`F5` to try the game and set ``main.tscn`` as your project's main scene.
- However, the player won't die yet. We'll work on that in the next part.
- .. |image0| image:: img/06.jump_and_squash/02.project_settings.png
- .. |image1| image:: img/06.jump_and_squash/03.physics_layers.webp
- .. |image2| image:: img/06.jump_and_squash/04.default_physics_properties.webp
- .. |image3| image:: img/06.jump_and_squash/05.toggle_layer_and_mask.webp
- .. |image4| image:: img/06.jump_and_squash/06.named_checkboxes.png
- .. |image5| image:: img/06.jump_and_squash/07.player_physics_mask.webp
- .. |image6| image:: img/06.jump_and_squash/08.mob_physics_mask.webp
- .. |image7| image:: img/06.jump_and_squash/09.groups_tab.png
- .. |image8| image:: img/06.jump_and_squash/10.group_scene_icon.png
|