Browse Source

Write 3D tutorial part 6: jump_and_squash

Nathan Lovato 4 years ago
parent
commit
ca031fc26b

+ 276 - 0
getting_started/first_3d_game/06.jump_and_squash.rst

@@ -0,0 +1,276 @@
+.. _doc_first_3d_game_jumping_and_squashing_monsters:
+
+Jumping and squashing monsters
+==============================
+
+In this part, we’ll add the ability to jump, to 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 ``0``.
+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 I mentioned above, the *Mask* property allows a node to listen to interaction
+with other physics objects, but we don’t need it to have collisions. The
+*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 the first layer is the
+“player” one.
+
+|image5|
+
+Then, open the *Mob* scene by double-clicking on ``Mob.tscn`` and select the
+*Mob* node.
+
+Set its *Collision -> Layer* to “enemies” and its *Collision -> Mask* to both
+“player”.
+
+|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``.
+
+::
+
+   #...
+   # Vertical impulse applied to the character upon jumping in meters per second.
+   export var jump_impulse = 20
+
+Inside ``_physics_process()``, add the following code before the line where we
+called ``move_and_slide()``.
+
+::
+
+   func _physics_process(delta):
+       #...
+
+       # Jumping.
+       if is_on_floor() and Input.is_action_just_pressed("jump"):
+           velocity.y += jump_impulse
+
+       #...
+
+That’s all you need to jump!
+
+The ``is_on_floor()`` method is a tool from the ``KinematicBody`` 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, feel
+great.
+
+Notice that the Y axis is positive upwards. That’s unlike 2D, where the Y axis
+is positive downward.
+
+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.
+
+::
+
+   # Vertical impulse applied to the character upon bouncing over a mob in
+   # meters per second.
+   export var bounce_impulse = 16
+
+Then, at the bottom of ``_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.
+
+::
+
+   func _physics_process(delta):
+       #...
+       for index in range(get_slide_count()):
+           # We check every collision that occurred this frame.
+           var collision = get_slide_collision(index)
+           # If we collide with a monster...
+           if collision.collider.is_in_group("mob"):
+               var mob = collision.collider
+               # ...we check that we are hitting it from above.
+               if Vector3.UP.dot(collision.normal) > 0.1:
+                   # If so, we squash it and bounce.
+                   mob.squash()
+                   velocity.y = bounce_impulse
+
+That’s a lot of new functions. Here’s some more information about them.
+
+The functions ``get_slide_count()`` and ``get_slide_collision()`` both come from
+the :ref:`KinematicBody<class_KinematicBody>` class and are related to
+``move_and_slide()``.
+
+``get_slide_collision()`` returns a
+:ref:`KinematicCollision<class_KinematicCollision>` object that holds
+information about where and how the collision occurred. For example, we use its
+``collider`` property to check if we collided with a “mob” by calling
+``is_in_group()`` on it: ``collision.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.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.
+
+We are calling one undefined function, ``mob.squash()``. 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.
+
+::
+
+   # Emitted when the player jumped on the mob.
+   signal squashed
+
+   # ...
+
+
+   func squash():
+       emit_signal("squashed")
+       queue_free()
+
+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
+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.png
+.. |image2| image:: img/06.jump_and_squash/04.default_physics_properties.png
+.. |image3| image:: img/06.jump_and_squash/05.toggle_layer_and_mask.png
+.. |image4| image:: img/06.jump_and_squash/06.named_checkboxes.png
+.. |image5| image:: img/06.jump_and_squash/07.player_physics_mask.png
+.. |image6| image:: img/06.jump_and_squash/08.mob_physics_mask.png
+.. |image7| image:: img/06.jump_and_squash/09.groups_tab.png
+.. |image8| image:: img/06.jump_and_squash/10.group_scene_icon.png

BIN
getting_started/first_3d_game/img/06.jump_and_squash/02.project_settings.png


BIN
getting_started/first_3d_game/img/06.jump_and_squash/03.physics_layers.png


BIN
getting_started/first_3d_game/img/06.jump_and_squash/04.default_physics_properties.png


BIN
getting_started/first_3d_game/img/06.jump_and_squash/05.toggle_layer_and_mask.png


BIN
getting_started/first_3d_game/img/06.jump_and_squash/06.named_checkboxes.png


BIN
getting_started/first_3d_game/img/06.jump_and_squash/07.player_physics_mask.png


BIN
getting_started/first_3d_game/img/06.jump_and_squash/08.mob_physics_mask.png


BIN
getting_started/first_3d_game/img/06.jump_and_squash/09.groups_tab.png


BIN
getting_started/first_3d_game/img/06.jump_and_squash/10.group_scene_icon.png


+ 1 - 0
getting_started/first_3d_game/index.rst

@@ -59,5 +59,6 @@ Contents
    03.player_movement_code
    03.player_movement_code
    04.mob_scene
    04.mob_scene
    05.spawning_mobs
    05.spawning_mobs
+   06.jump_and_squash
 
 
 .. |image0| image:: img/squash-the-creeps-final.gif
 .. |image0| image:: img/squash-the-creeps-final.gif