Quellcode durchsuchen

Merge pull request #4724 from paulloz/c#-first-3d-game

Add C# code tabs to pages under first_3d_game
Rémi Verschelde vor 4 Jahren
Ursprung
Commit
305ec49b60

+ 139 - 6
getting_started/first_3d_game/03.player_movement_code.rst

@@ -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
 -----------------------------
 

+ 106 - 5
getting_started/first_3d_game/04.mob_scene.rst

@@ -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

+ 72 - 3
getting_started/first_3d_game/05.spawning_mobs.rst

@@ -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.
 

+ 83 - 6
getting_started/first_3d_game/06.jump_and_squash.rst

@@ -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

+ 222 - 5
getting_started/first_3d_game/07.killing_player.rst

@@ -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

+ 110 - 7
getting_started/first_3d_game/08.score_and_replay.rst

@@ -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

+ 195 - 5
getting_started/first_3d_game/09.adding_animations.rst

@@ -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