add C# code tabs to pages under first_3d_game

This commit is contained in:
Paul Joannon
2021-03-05 10:08:34 +01:00
parent 0a087e730f
commit b7e9a081fd
7 changed files with 927 additions and 37 deletions

View File

@@ -16,7 +16,8 @@ Let's start with the class's properties. We're going to define a movement speed,
a fall acceleration representing gravity, and a velocity we'll use to move the
character.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
@@ -27,6 +28,23 @@ character.
var velocity = Vector3.ZERO
.. code-tab:: csharp
public class Player : KinematicBody
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
// 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;
private Vector3 _velocity = Vector3.Zero;
}
These are common properties for a moving body. The ``velocity`` is a 3D vector
combining a speed with a direction. Here, we define it as a property because
we want to update and reuse its value across frames.
@@ -40,7 +58,8 @@ we want to update and reuse its value across frames.
Let's code the movement now. We start by calculating the input direction vector
using the global ``Input`` object, in ``_physics_process()``.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
# We create a local variable to store the input direction.
@@ -58,6 +77,34 @@ using the global ``Input`` object, in ``_physics_process()``.
if Input.is_action_pressed("move_forward"):
direction.z -= 1
.. code-tab:: csharp
public override void _PhysicsProcess(float 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.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("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 += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
}
Here, we're going to make all calculations using the ``_physics_process()``
virtual function. Like ``_process()``, it allows you to update the node every
frame, but it's designed specifically for physics-related code like moving a
@@ -80,7 +127,8 @@ have a length of about ``1.4``. But if they press a single key, it will have a
length of ``1``. We want the vector's length to be consistent. To do so, we can
call its ``normalize()`` method.
::
.. tabs::
.. code-tab:: gdscript GDScript
#func _physics_process(delta):
#...
@@ -89,6 +137,19 @@ call its ``normalize()`` method.
direction = direction.normalized()
$Pivot.look_at(translation + direction, Vector3.UP)
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
}
}
Here, we only normalize the vector if the direction has a length greater than
zero, which means the player is pressing a direction key.
@@ -110,9 +171,10 @@ Then, we update the velocity. We have to calculate the ground velocity and the
fall speed separately. Be sure to go back one tab so the lines are inside the
``_physics_process()`` function but outside the condition we just wrote.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):_
func _physics_process(delta):
#...
if direction != Vector3.ZERO:
#...
@@ -125,6 +187,21 @@ fall speed separately. Be sure to go back one tab so the lines are inside the
# Moving the character
velocity = move_and_slide(velocity, Vector3.UP)
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
// Ground velocity
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Vertical velocity
_velocity.y -= FallAcceleration * delta;
// Moving the character
_velocity = MoveAndSlide(_velocity, Vector3.Up);
}
For the vertical velocity, we subtract the fall acceleration multiplied by the
delta time every frame. Notice the use of the ``-=`` operator, which is a
shorthand for ``variable = variable - ...``.
@@ -153,7 +230,8 @@ And that's all the code you need to move the character on the floor.
Here is the complete ``Player.gd`` code for reference.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
@@ -186,6 +264,61 @@ Here is the complete ``Player.gd`` code for reference.
velocity.y -= fall_acceleration * delta
velocity = move_and_slide(velocity, Vector3.UP)
.. code-tab:: csharp
public class Player : KinematicBody
{
// 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;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float 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.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("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 += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
}
// Ground velocity
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Vertical velocity
_velocity.y -= FallAcceleration * delta;
// Moving the character
_velocity = MoveAndSlide(_velocity, Vector3.Up);
}
}
Testing our player's movement
-----------------------------

View File

@@ -97,7 +97,8 @@ Here's the movement code to start with. We define two properties, ``min_speed``
and ``max_speed``, to define a random speed range. We then define and initialize
the ``velocity``.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
@@ -112,6 +113,27 @@ the ``velocity``.
func _physics_process(_delta):
move_and_slide(velocity)
.. code-tab:: csharp
public class Mob : KinematicBody
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
// 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);
}
}
Similarly to the player, we move the mob every frame by calling
``KinematicBody``\ 's ``move_and_slide()`` method. This time, we don't update
the ``velocity`` every frame: we want the monster to move at a constant speed
@@ -129,7 +151,8 @@ player using the ``look_at()`` method and randomize the angle by rotating a
random amount around the Y axis. Below, ``rand_range()`` outputs a random value
between ``-PI / 4`` radians and ``PI / 4`` radians.
::
.. tabs::
.. code-tab:: gdscript GDScript
# We will call this function from the Main scene.
func initialize(start_position, player_position):
@@ -139,6 +162,18 @@ between ``-PI / 4`` radians and ``PI / 4`` radians.
# And rotate it randomly so it doesn't move exactly toward the player.
rotate_y(rand_range(-PI / 4, PI / 4))
.. code-tab:: csharp
// We will call this function from the Main scene
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
Translation = startPosition;
// We turn the mob so it looks at the player.
LookAt(playerPosition, Vector3.Up);
// And rotate it randomly so it doesn't move exactly toward the player.
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
}
We then calculate a random speed using ``rand_range()`` once again and we use it
to calculate the velocity.
@@ -146,7 +181,11 @@ We start by creating a 3D vector pointing forward, multiply it by our
``random_speed``, and finally rotate it using the ``Vector3`` class's
``rotated()`` method.
::
.. tabs::
.. code-tab:: gdscript GDScript
func initialize(start_position, player_position):
# ...
# We calculate a random speed.
var random_speed = rand_range(min_speed, max_speed)
@@ -155,6 +194,20 @@ We start by creating a 3D vector pointing forward, multiply it by our
# We then rotate the vector based on the mob's Y rotation to move in the direction it's looking.
velocity = velocity.rotated(Vector3.UP, rotation.y)
.. code-tab:: csharp
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// ...
// We calculate a random speed.
float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
// We calculate a forward velocity that represents the speed.
_velocity = Vector3.Forward * randomSpeed;
// We then rotate the vector based on the mob's Y rotation to move in the direction it's looking
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
Leaving the screen
------------------
@@ -180,17 +233,28 @@ This will take you back to the script editor and add a new function for you,
method. This will destroy the mob instance when the *VisibilityNotifier* \'s box
leaves the screen.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_VisibilityNotifier_screen_exited():
queue_free()
.. code-tab:: csharp
// We also specified this function name in PascalCase in the editor's connection window
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
Our monster is ready to enter the game! In the next part, you will spawn
monsters in the game level.
Here is the complete ``Mob.gd`` script for reference.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
@@ -218,6 +282,43 @@ Here is the complete ``Mob.gd`` script for reference.
func _on_VisibilityNotifier_screen_exited():
queue_free()
.. code-tab:: csharp
public class Mob : KinematicBody
{
// 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);
}
// We will call this function from the Main scene
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
Translation = startPosition;
LookAt(playerPosition, Vector3.Up);
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
var randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
_velocity = Vector3.Forward * randomSpeed;
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
// We also specified this function name in PascalCase in the editor's connection window
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}
.. |image0| image:: img/04.mob_scene/01.initial_three_nodes.png
.. |image1| image:: img/04.mob_scene/02.add_child_node.png
.. |image2| image:: img/04.mob_scene/03.scene_with_collision_shape.png

View File

@@ -158,7 +158,8 @@ Then, as we're going to spawn the monsters procedurally, we want to randomize
numbers every time we play the game. If we don't do that, the monsters will
always spawn following the same sequence.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
@@ -168,6 +169,24 @@ always spawn following the same sequence.
func _ready():
randomize()
.. code-tab:: csharp
public class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
#pragma warning disable 649
// We assign this in the editor, so we don't need the warning about not being assigned.
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
}
}
We want to spawn mobs at regular time intervals. To do this, we need to go back
to the scene and add a timer. Before that, though, we need to assign the
``Mob.tscn`` file to the ``mob_scene`` property.
@@ -212,7 +231,8 @@ Let's code the mob spawning logic. We're going to:
5. Call the mob's ``initialize()`` method, passing it the random position and
the player's position.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_MobTimer_timeout():
# Create a Mob instance and add it to the scene.
@@ -229,12 +249,33 @@ Let's code the mob spawning logic. We're going to:
add_child(mob)
mob.initialize(mob_spawn_location.translation, player_position)
.. code-tab:: csharp
// We also specified this function name in PascalCase in the editor's connection window
public void OnMobTimerTimeout()
{
// Create a mob instance and add it to the scene.
Mob mob = MobScene.Instance();
// Choose a random location on Path2D.
// We stire the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
AddChild(mob);
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
}
Above, ``randf()`` produces a random value between ``0`` and ``1``, which is
what the *PathFollow* node's ``unit_offset`` expects.
Here is the complete ``Main.gd`` script so far, for reference.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
@@ -255,6 +296,34 @@ Here is the complete ``Main.gd`` script so far, for reference.
add_child(mob)
mob.initialize(mob_spawn_location.translation, player_position)
.. code-tab:: csharp
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()
{
Mob mob = MobScene.Instance();
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
AddChild(mob);
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
}
}
You can test the scene by pressing :kbd:`F6`. You should see the monsters spawn and
move in a straight line.

View File

@@ -109,16 +109,27 @@ script. We need a value to control the jump's strength and update
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 = 20;
Inside ``_physics_process()``, add the following code before the line where we
called ``move_and_slide()``.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
#...
@@ -129,6 +140,21 @@ called ``move_and_slide()``.
#...
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_velocity.y += JumpImpulse;
}
// ...
}
That's all you need to jump!
The ``is_on_floor()`` method is a tool from the ``KinematicBody`` class. It
@@ -181,12 +207,21 @@ 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 = 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
@@ -197,7 +232,8 @@ 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):
#...
@@ -213,7 +249,31 @@ With this code, if no collisions occurred on a given frame, the loop won't run.
mob.squash()
velocity.y = bounce_impulse
That's a lot of new functions. Here's some more information about them.
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
for (int index = 0; i < GetSlideCount(); index++)
{
// We check every collision that occurred this frame.
KinematicCollision collision = GetSlideCollision(index);
// If we collide with a monster...
if (collision.Collider is Mob mob && mob.IsInGroup("mob"))
{
// ...we check that we are hitting it from above.
if (Vector3.Up.Dot(collision.Normal) > 0.1f)
{
// If so, we squash it and bounce.
mob.Squash();
_velocity.y = BounceImpulse;
}
}
}
}
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
@@ -246,7 +306,8 @@ 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
@@ -258,6 +319,22 @@ destroy the mob.
emit_signal("squashed")
queue_free()
.. code-tab:: csharp
// Don't forget to rebuild the project so the editor knows about the new signal.
// Emitted when the played jumped on the mob.
[Signal]
public delegate void Squashed();
// ...
public void Squash()
{
EmitSignal(nameof(Squashed));
QueueFree();
}
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

View File

@@ -60,7 +60,8 @@ 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.
::
.. tabs::
.. code-tab:: gdscript GDScript
# Emitted when the player was hit by a mob.
# Put this at the top of the script.
@@ -76,6 +77,28 @@ a ``die()`` function that helps us put a descriptive label on the code.
func _on_MobDetector_body_entered(_body):
die()
.. code-tab:: csharp
// 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();
}
Try the game again by pressing :kbd:`F5`. If everything is set up correctly,
the character should die when an enemy runs into it.
@@ -97,11 +120,20 @@ connect its ``hit`` signal to the *Main* node.
Get and stop the timer in the ``_on_Player_hit()`` function.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Player_hit():
$MobTimer.stop()
.. code-tab:: csharp
// We also specified this function name in PascalCase in the editor's connection window
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
If you try the game now, the monsters will stop spawning when you die,
and the remaining ones will leave the screen.
@@ -120,7 +152,8 @@ for reference. You can use them to compare and check your code.
Starting with ``Main.gd``.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
@@ -137,6 +170,7 @@ Starting with ``Main.gd``.
# Choose a random location on Path2D.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.unit_offset = randf()
var player_position = $Player.transform.origin
@@ -148,12 +182,54 @@ Starting with ``Main.gd``.
func _on_Player_hit():
$MobTimer.stop()
.. code-tab:: csharp
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 mob instance and add it to the scene.
var mob = (Mob)MobScene.Instance();
// Choose a random location on Path2D.
// We stire the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
AddChild(mob);
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
}
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
}
Next is ``Mob.gd``.
::
.. tabs::
.. code-tab:: gdscript GDScript
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.
@@ -176,12 +252,64 @@ Next is ``Mob.gd``.
velocity = velocity.rotated(Vector3.UP, rotation.y)
func squash():
emit_signal("squashed")
queue_free()
func _on_VisibilityNotifier_screen_exited():
queue_free()
.. code-tab:: csharp
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)
{
Translation = startPosition;
LookAt(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();
}
}
Finally, the longest script, ``Player.gd``.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
@@ -243,6 +371,95 @@ Finally, the longest script, ``Player.gd``.
func _on_MobDetector_body_entered(_body):
die()
.. code-tab:: csharp
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();
}
}
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

View File

@@ -90,12 +90,20 @@ Keeping track of the score
Let's work on the score next. Attach a new script to the *ScoreLabel* and define
the ``score`` variable.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends Label
var score = 0
.. code-tab:: csharp
public class ScoreLabel : Label
{
private int _score = 0;
}
The score should increase by ``1`` every time we squash a monster. We can use
their ``squashed`` signal to know when that happens. However, as we instantiate
monsters from the code, we cannot do the connection in the editor.
@@ -114,13 +122,23 @@ dock.
At the bottom of the ``_on_MobTimer_timeout()`` function, add the following
line.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_MobTimer_timeout():
#...
# We connect the mob to the score label to update the score upon squashing one.
mob.connect("squashed", $UserInterface/ScoreLabel, "_on_Mob_squashed")
.. code-tab:: csharp
public void OnMobTimerTimeout()
{
// ...
// We connect the mob to the score label to update the score upon squashing one.
mob.Connect(nameof(Mob.Squashed), GetNode<ScoreLabel>("UserInterface/ScoreLabel"), nameof(ScoreLabel.OnMobSquashed));
}
This line means that when the mob emits the ``squashed`` signal, the
*ScoreLabel* node will receive it and call the function ``_on_Mob_squashed()``.
@@ -129,12 +147,21 @@ callback function.
There, we increment the score and update the displayed text.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Mob_squashed():
score += 1
text = "Score: %s" % score
.. code-tab:: csharp
public void OnMobSquashed()
{
_score += 1;
Text = string.Format("Score: {0}", _score);
}
The second line uses the value of the ``score`` variable to replace the
placeholder ``%s``. When using this feature, Godot automatically converts values
to text, which is convenient to output text in labels or using the ``print()``
@@ -215,20 +242,38 @@ dies and plays again.
Open the script ``Main.gd``. First, we want to hide the overlay at the start of
the game. Add this line to the ``_ready()`` function.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _ready():
#...
$UserInterface/Retry.hide()
.. code-tab:: csharp
public override void _Ready()
{
// ...
GetNode<Control>("UserInterface/Retry").Hide();
}
Then, when the player gets hit, we show the overlay.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Player_hit():
#...
$UserInterface/Retry.show()
.. code-tab:: csharp
public void OnPlayerHit()
{
//...
GetNode<Control>("UserInterface/Retry").Show();
}
Finally, when the *Retry* node is visible, we need to listen to the player's
input and restart the game if they press enter. To do this, we use the built-in
``_unhandled_input()`` callback.
@@ -236,13 +281,25 @@ input and restart the game if they press enter. To do this, we use the built-in
If the player pressed the predefined ``ui_accept`` input action and *Retry* is
visible, we reload the current scene.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _unhandled_input(event):
if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
# This restarts the current scene.
get_tree().reload_current_scene()
.. code-tab:: csharp
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("ui_accept") && GetNode<Control>("UserInterface/Retry").Visible)
{
// This restarts the current scene.
GetTree().ReloadCurrentScene();
}
}
The function ``get_tree()`` gives us access to the global :ref:`SceneTree
<class_SceneTree>` object, which allows us to reload and restart the current
scene.
@@ -312,7 +369,8 @@ make the game both look and feel much nicer.
Here is the complete ``Main.gd`` script for reference.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
@@ -346,6 +404,51 @@ Here is the complete ``Main.gd`` script for reference.
$MobTimer.stop()
$UserInterface/Retry.show()
.. code-tab:: csharp
public class Main : Node
{
#pragma warning disable 649
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
GetNode<Control>("UserInterface/Retry").Hide();
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("ui_accept") && GetNode<Control>("UserInterface/Retry").Visible)
{
GetTree().ReloadCurrentScene();
}
}
public void OnMobTimerTimeout()
{
Mob mob = MobScene.Instance();
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
AddChild(mob);
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
mob.Connect(nameof(Mob.Squashed), GetNode<ScoreLabel>("UserInterface/ScoreLabel"), nameof(ScoreLabel.OnMobSquashed));
}
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
GetNode<Control>("UserInterface/Retry").Show();
}
}
.. |image0| image:: img/08.score_and_replay/01.label_node.png
.. |image1| image:: img/08.score_and_replay/02.score_placeholder.png
.. |image2| image:: img/08.score_and_replay/02.score_custom_color.png

View File

@@ -186,7 +186,8 @@ Open the *Player*'s script by clicking the script icon next to it.
In ``_physics_process()``, after the line where we check the ``direction``
vector, add the following code.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
#...
@@ -196,6 +197,22 @@ vector, add the following code.
else:
$AnimationPlayer.playback_speed = 1
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
if (direction != Vector3.Zero)
{
// ...
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
}
else
{
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
}
}
This code makes it so when the player moves, we multiply the playback speed by
``4``. When they stop, we reset it to normal.
@@ -203,12 +220,22 @@ We mentioned that the pivot could layer transforms on top of the animation. We
can make the character arc when jumping using the following line of code. Add it
at the end of ``_physics_process()``.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
#...
$Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
var pivot = GetNode<Spatial>("Pivot");
pivot.Rotation = new Vector3(Mathf.Pi / 6f * _velocity.y / JumpImpulse, pivot.Rotation.y, pivot.Rotation.z);
}
Animating the mobs
------------------
@@ -233,12 +260,21 @@ We can change the playback speed based on the creature's ``random_speed``. Open
the *Mob*'s script and at the end of the ``initialize()`` function, add the
following line.
::
.. tabs::
.. code-tab:: gdscript GDScript
func initialize(start_position, player_position):
#...
$AnimationPlayer.playback_speed = random_speed / min_speed
.. code-tab:: csharp
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// ...
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = randomSpeed / MinSpeed;
}
And with that, you finished coding your first complete 3D game.
**Congratulations**!
@@ -249,7 +285,8 @@ to keep learning more. But for now, here are the complete ``Player.gd`` and
Here's the *Player* script.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
@@ -316,12 +353,113 @@ Here's the *Player* script.
func _on_MobDetector_body_entered(_body):
die()
.. code-tab:: csharp
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);
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
}
else
{
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
}
_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;
}
}
}
var pivot = GetNode<Spatial>("Pivot");
pivot.Rotation = new Vector3(Mathf.Pi / 6f * _velocity.y / JumpImpulse, pivot.Rotation.y, pivot.Rotation.z);
}
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
}
And the *Mob*'s script.
::
.. tabs::
.. code-tab:: gdscript GDScript
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.
@@ -346,9 +484,61 @@ And the *Mob*'s script.
$AnimationPlayer.playback_speed = random_speed / min_speed
func squash():
emit_signal("squashed")
queue_free()
func _on_VisibilityNotifier_screen_exited():
queue_free()
.. code-tab:: csharp
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)
{
Translation = startPosition;
LookAt(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);
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = randomSpeed / MinSpeed;
}
public void Squash()
{
EmitSignal(nameof(Squashed));
QueueFree();
}
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}
.. |image0| image:: img/squash-the-creeps-final.gif
.. |image1| image:: img/09.adding_animations/01.animation_player_dock.png
.. |image2| image:: img/09.adding_animations/02.new_animation.png