Browse Source

Write part 7 of the 3D tutorial: killing the player

Nathan Lovato 4 years ago
parent
commit
e8d1837989

+ 253 - 0
getting_started/first_3d_game/07.killing_player.rst

@@ -0,0 +1,253 @@
+.. _doc_first_3d_game_killing_the_player:
+
+Killing the player
+==================
+
+We can kill enemies by jumping on them, but the player still can't die.
+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
+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.
+
+|image0|
+
+In the *Inspector*, assign a cylinder shape to it.
+
+|image1|
+
+Here is a trick you can use to make the collisions only happen when the
+player is on the ground or close to it. You can reduce the cylinder's
+height and move it up to the top of the character. This way, when the
+player jumps, the shape will be too high up for the enemies to collide
+with it.
+
+|image2|
+
+You also want the cylinder to be wider than the sphere. This way, the
+player gets hit before colliding and being pushed on top of the
+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
+cannot detect the area. The complementary *Monitoring* property allows
+it to detect collisions. Then, remove the *Collision -> Layer* and sets
+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*.
+
+|image4|
+
+The *MobDetector* will emit ``body_entered`` when a *KinematicBody* or a
+*RigidBody* 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
+a ``die()`` function that helps us put a descriptive label on the code.
+
+::
+
+   # 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()
+
+Try the game again by pressing F5. If everything is set up correctly,
+the character should die when an enemy runs into it.
+
+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
+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
+reaction.
+
+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.
+
+::
+
+   func _on_Player_hit():
+       $MobTimer.stop()
+
+If you try the game now, the monsters will stop spawning when you die,
+and the remaining ones will leave the screen.
+
+You can pat yourself in the back: you prototyped a complete 3D game,
+even if it's still a bit rough.
+
+From there, we'll add a score, the option to retry the game, and you'll
+see how you can make the game feel much more alive with minimalistic
+animations.
+
+Code checkpoint
+---------------
+
+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``.
+
+::
+
+   extends Node
+
+   export (PackedScene) var mob_scene
+
+
+   func _ready():
+       randomize()
+
+
+   func _on_MobTimer_timeout():
+       # Create a Mob instance and add it to the scene.
+       var mob = mob_scene.instance()
+
+       # Choose a random location on Path2D.
+       var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
+       mob_spawn_location.unit_offset = randf()
+
+       var player_position = $Player.transform.origin
+
+       add_child(mob)
+       mob.initialize(mob_spawn_location.translation, player_position)
+
+
+   func _on_Player_hit():
+       $MobTimer.stop()
+
+Next is ``Mob.gd``.
+
+::
+
+   extends KinematicBody
+
+   # 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):
+       translation = start_position
+       look_at(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_VisibilityNotifier_screen_exited():
+       queue_free()
+
+Finally, the longest script, ``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_down"):
+           direction.z += 1
+       if Input.is_action_pressed("move_up"):
+           direction.z -= 1
+
+       if direction.length() > 0:
+           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()
+
+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
+.. |image4| image:: img/07.killing_player/05.body_entered_signal.png
+.. |image5| image:: img/07.killing_player/06.player_hit_signal.png

BIN
getting_started/first_3d_game/img/07.killing_player/01.adding_area_node.png


BIN
getting_started/first_3d_game/img/07.killing_player/02.cylinder_shape.png


BIN
getting_started/first_3d_game/img/07.killing_player/03.cylinder_in_editor.png


BIN
getting_started/first_3d_game/img/07.killing_player/04.mob_detector_properties.png


BIN
getting_started/first_3d_game/img/07.killing_player/05.body_entered_signal.png


BIN
getting_started/first_3d_game/img/07.killing_player/06.player_hit_signal.png


+ 1 - 0
getting_started/first_3d_game/index.rst

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