mirror of
https://github.com/godotengine/godot-docs.git
synced 2026-01-04 14:11:02 +03:00
add C# code tabs to pages under first_3d_game
This commit is contained in:
@@ -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
|
||||
-----------------------------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user