|
@@ -176,17 +176,17 @@ Start by declaring the member variables this object will need:
|
|
|
|
|
|
extends Area2D
|
|
|
|
|
|
- export var speed = 400 # How fast the player will move (pixels/sec).
|
|
|
- var screen_size # Size of the game window.
|
|
|
+ export var speed = 400 # How fast the player will move (pixels/sec).
|
|
|
+ var screen_size # Size of the game window.
|
|
|
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
public class Player : Area2D
|
|
|
{
|
|
|
[Export]
|
|
|
- public int Speed = 400; // How fast the player will move (pixels/sec).
|
|
|
+ public int speed = 400; // How fast the player will move (pixels/sec).
|
|
|
|
|
|
- private Vector2 _screenSize; // Size of the game window.
|
|
|
+ public Vector2 screenSize; // Size of the game window.
|
|
|
}
|
|
|
|
|
|
|
|
@@ -213,12 +213,14 @@ which is a good time to find the size of the game window:
|
|
|
|
|
|
func _ready():
|
|
|
screen_size = get_viewport_rect().size
|
|
|
+ hide() # Start the player hidden.
|
|
|
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
public override void _Ready()
|
|
|
{
|
|
|
- _screenSize = GetViewport().Size;
|
|
|
+ screenSize = GetViewportRect().Size;
|
|
|
+ Hide(); // Start the player hidden.
|
|
|
}
|
|
|
|
|
|
Now we can use the ``_process()`` function to define what the player will do.
|
|
@@ -233,9 +235,9 @@ need to do the following:
|
|
|
First, we need to check for input - is the player pressing a key? For
|
|
|
this game, we have 4 direction inputs to check. Input actions are defined
|
|
|
in the Project Settings under "Input Map". Here, you can define custom events and
|
|
|
-assign different keys, mouse events, or other inputs to them. For this demo,
|
|
|
-we will use the default events that are assigned to the arrow keys on the
|
|
|
-keyboard.
|
|
|
+assign different keys, mouse events, or other inputs to them.
|
|
|
+For this game, we will just use the default events called
|
|
|
+"ui_right" etc that are assigned to the arrow keys on the keyboard.
|
|
|
|
|
|
You can detect whether a key is pressed using
|
|
|
``Input.is_action_pressed()``, which returns ``true`` if it's pressed
|
|
@@ -245,7 +247,7 @@ or ``false`` if it isn't.
|
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
|
|
func _process(delta):
|
|
|
- var velocity = Vector2() # The player's movement vector.
|
|
|
+ var velocity = Vector2.ZERO # The player's movement vector.
|
|
|
if Input.is_action_pressed("ui_right"):
|
|
|
velocity.x += 1
|
|
|
if Input.is_action_pressed("ui_left"):
|
|
@@ -254,6 +256,7 @@ or ``false`` if it isn't.
|
|
|
velocity.y += 1
|
|
|
if Input.is_action_pressed("ui_up"):
|
|
|
velocity.y -= 1
|
|
|
+
|
|
|
if velocity.length() > 0:
|
|
|
velocity = velocity.normalized() * speed
|
|
|
$AnimatedSprite.play()
|
|
@@ -264,7 +267,7 @@ or ``false`` if it isn't.
|
|
|
|
|
|
public override void _Process(float delta)
|
|
|
{
|
|
|
- var velocity = new Vector2(); // The player's movement vector.
|
|
|
+ var velocity = Vector2.Zero; // The player's movement vector.
|
|
|
|
|
|
if (Input.IsActionPressed("ui_right"))
|
|
|
{
|
|
@@ -290,7 +293,7 @@ or ``false`` if it isn't.
|
|
|
|
|
|
if (velocity.Length() > 0)
|
|
|
{
|
|
|
- velocity = velocity.Normalized() * Speed;
|
|
|
+ velocity = velocity.Normalized() * speed;
|
|
|
animatedSprite.Play();
|
|
|
}
|
|
|
else
|
|
@@ -339,8 +342,8 @@ the ``_process`` function (make sure it's not indented under the `else`):
|
|
|
|
|
|
Position += velocity * delta;
|
|
|
Position = new Vector2(
|
|
|
- x: Mathf.Clamp(Position.x, 0, _screenSize.x),
|
|
|
- y: Mathf.Clamp(Position.y, 0, _screenSize.y)
|
|
|
+ x: Mathf.Clamp(Position.x, 0, screenSize.x),
|
|
|
+ y: Mathf.Clamp(Position.y, 0, screenSize.y)
|
|
|
);
|
|
|
|
|
|
|
|
@@ -377,7 +380,7 @@ function:
|
|
|
if velocity.x != 0:
|
|
|
$AnimatedSprite.animation = "walk"
|
|
|
$AnimatedSprite.flip_v = false
|
|
|
- # See the note below about boolean assignment
|
|
|
+ # See the note below about boolean assignment.
|
|
|
$AnimatedSprite.flip_h = velocity.x < 0
|
|
|
elif velocity.y != 0:
|
|
|
$AnimatedSprite.animation = "up"
|
|
@@ -389,7 +392,7 @@ function:
|
|
|
{
|
|
|
animatedSprite.Animation = "walk";
|
|
|
animatedSprite.FlipV = false;
|
|
|
- // See the note below about boolean assignment
|
|
|
+ // See the note below about boolean assignment.
|
|
|
animatedSprite.FlipH = velocity.x < 0;
|
|
|
}
|
|
|
else if (velocity.y != 0)
|
|
@@ -486,8 +489,9 @@ this code to the function:
|
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
|
|
func _on_Player_body_entered(body):
|
|
|
- hide() # Player disappears after being hit.
|
|
|
+ hide() # Player disappears after being hit.
|
|
|
emit_signal("hit")
|
|
|
+ # Must be deferred as we can't change physics properties on a physics callback.
|
|
|
$CollisionShape2D.set_deferred("disabled", true)
|
|
|
|
|
|
.. code-tab:: csharp
|
|
@@ -495,7 +499,8 @@ this code to the function:
|
|
|
public void OnPlayerBodyEntered(PhysicsBody2D body)
|
|
|
{
|
|
|
Hide(); // Player disappears after being hit.
|
|
|
- EmitSignal("Hit");
|
|
|
+ EmitSignal(nameof(Hit));
|
|
|
+ // Must be deferred as we can't change physics properties on a physics callback.
|
|
|
GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
|
|
|
}
|
|
|
|
|
@@ -605,33 +610,31 @@ Add a script to the ``Mob`` and add the following member variables:
|
|
|
|
|
|
[Export]
|
|
|
public int MaxSpeed = 250; // Maximum speed range.
|
|
|
-
|
|
|
}
|
|
|
|
|
|
When we spawn a mob, we'll pick a random value between ``min_speed`` and
|
|
|
``max_speed`` for how fast each mob will move (it would be boring if they
|
|
|
were all moving at the same speed).
|
|
|
|
|
|
-Now let's look at the rest of the script. In ``_ready()`` we randomly
|
|
|
-choose one of the three animation types:
|
|
|
+Now let's look at the rest of the script. In ``_ready()`` we play the
|
|
|
+animation and randomly choose one of the three animation types:
|
|
|
|
|
|
.. tabs::
|
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
|
|
func _ready():
|
|
|
+ $AnimatedSprite.playing = true
|
|
|
var mob_types = $AnimatedSprite.frames.get_animation_names()
|
|
|
$AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
|
|
|
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
- // C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
|
|
|
- static private Random _random = new Random();
|
|
|
-
|
|
|
public override void _Ready()
|
|
|
{
|
|
|
var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
|
|
|
- var mobTypes = animSprite.Frames.GetAnimationNames();
|
|
|
- animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
|
|
|
+ animSprite.Playing = true;
|
|
|
+ string[] mobTypes = animSprite.Frames.GetAnimationNames();
|
|
|
+ animSprite.Animation = mobTypes[GD.Randi() % mobTypes.Length];
|
|
|
}
|
|
|
|
|
|
First, we get the list of animation names from the AnimatedSprite's ``frames``
|
|
@@ -740,7 +743,7 @@ instance.
|
|
|
|
|
|
extends Node
|
|
|
|
|
|
- export (PackedScene) var Mob
|
|
|
+ export(PackedScene) var mob_scene
|
|
|
var score
|
|
|
|
|
|
func _ready():
|
|
@@ -752,22 +755,17 @@ instance.
|
|
|
{
|
|
|
// 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 Mob;
|
|
|
+ public PackedScene mobScene;
|
|
|
+ #pragma warning restore 649
|
|
|
|
|
|
- private int _score;
|
|
|
-
|
|
|
- // We use 'System.Random' as an alternative to GDScript's random methods.
|
|
|
- private Random _random = new Random();
|
|
|
+ public int score;
|
|
|
|
|
|
public override void _Ready()
|
|
|
{
|
|
|
- }
|
|
|
-
|
|
|
- // We'll use this later because C# doesn't support GDScript's randi().
|
|
|
- private float RandRange(float min, float max)
|
|
|
- {
|
|
|
- return (float)_random.NextDouble() * (max - min) + min;
|
|
|
+ GD.Randomize();
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -816,7 +814,7 @@ for a new game:
|
|
|
|
|
|
public void NewGame()
|
|
|
{
|
|
|
- _score = 0;
|
|
|
+ score = 0;
|
|
|
|
|
|
var player = GetNode<Player>("Player");
|
|
|
var startPosition = GetNode<Position2D>("StartPosition");
|
|
@@ -849,7 +847,7 @@ the other two timers. ``ScoreTimer`` will increment the score by 1.
|
|
|
|
|
|
public void OnScoreTimerTimeout()
|
|
|
{
|
|
|
- _score++;
|
|
|
+ score++;
|
|
|
}
|
|
|
|
|
|
In ``_on_MobTimer_timeout()``, we will create a mob instance, pick a
|
|
@@ -865,49 +863,62 @@ Note that a new instance must be added to the scene using ``add_child()``.
|
|
|
|
|
|
func _on_MobTimer_timeout():
|
|
|
# Choose a random location on Path2D.
|
|
|
- $MobPath/MobSpawnLocation.offset = randi()
|
|
|
+ var mob_spawn_location = get_node("MobPath/MobSpawnLocation");
|
|
|
+ mob_spawn_location.offset = randi()
|
|
|
+
|
|
|
# Create a Mob instance and add it to the scene.
|
|
|
- var mob = Mob.instance()
|
|
|
+ var mob = mob_scene.instance()
|
|
|
add_child(mob)
|
|
|
+
|
|
|
# Set the mob's direction perpendicular to the path direction.
|
|
|
- var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
|
|
|
+ var direction = mob_spawn_location.rotation + PI / 2
|
|
|
+
|
|
|
# Set the mob's position to a random location.
|
|
|
- mob.position = $MobPath/MobSpawnLocation.position
|
|
|
+ mob.position = mob_spawn_location.position
|
|
|
+
|
|
|
# Add some randomness to the direction.
|
|
|
direction += rand_range(-PI / 4, PI / 4)
|
|
|
mob.rotation = direction
|
|
|
- # Set the velocity (speed & direction).
|
|
|
- mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
|
|
|
- mob.linear_velocity = mob.linear_velocity.rotated(direction)
|
|
|
+
|
|
|
+ # Choose the velocity.
|
|
|
+ var velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
|
|
|
+ mob.linear_velocity = velocity.rotated(direction)
|
|
|
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
public void OnMobTimerTimeout()
|
|
|
{
|
|
|
+ // Note: Normally it is best to use explicit types rather than the `var`
|
|
|
+ // keyword. However, var is acceptable to use here because the types are
|
|
|
+ // obviously PathFollow2D and Mob, since they appear later on the line.
|
|
|
+
|
|
|
// Choose a random location on Path2D.
|
|
|
var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
|
|
|
- mobSpawnLocation.Offset = _random.Next();
|
|
|
+ mobSpawnLocation.Offset = GD.Randi();
|
|
|
|
|
|
// Create a Mob instance and add it to the scene.
|
|
|
- var mobInstance = (RigidBody2D)Mob.Instance();
|
|
|
- AddChild(mobInstance);
|
|
|
+ var mob = (Mob)mobScene.Instance();
|
|
|
+ AddChild(mob);
|
|
|
|
|
|
// Set the mob's direction perpendicular to the path direction.
|
|
|
float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;
|
|
|
|
|
|
// Set the mob's position to a random location.
|
|
|
- mobInstance.Position = mobSpawnLocation.Position;
|
|
|
+ mob.Position = mobSpawnLocation.Position;
|
|
|
|
|
|
// Add some randomness to the direction.
|
|
|
- direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
|
|
|
- mobInstance.Rotation = direction;
|
|
|
+ direction += (float)GD.RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
|
|
|
+ mob.Rotation = direction;
|
|
|
|
|
|
// Choose the velocity.
|
|
|
- mobInstance.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
|
|
|
+ var velocity = new Vector2((float)GD.RandRange(mob.minSpeed, mob.maxSpeed), 0);
|
|
|
+ mob.LinearVelocity = velocity.Rotated(direction);
|
|
|
}
|
|
|
|
|
|
-.. important:: Why ``PI``? In functions requiring angles, GDScript uses *radians*,
|
|
|
- not degrees. If you're more comfortable working with
|
|
|
+.. important:: Why ``PI``? In functions requiring angles, Godot uses *radians*,
|
|
|
+ not degrees. Pi represents a half turn in radians, about
|
|
|
+ ``3.1415`` (there is also ``TAU`` which is equal to ``2 * PI``).
|
|
|
+ If you're more comfortable working with
|
|
|
degrees, you'll need to use the ``deg2rad()`` and
|
|
|
``rad2deg()`` functions to convert between the two.
|
|
|
|
|
@@ -925,10 +936,9 @@ Let's test the scene to make sure everything is working. Add this to ``_ready()`
|
|
|
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
- public override void _Ready()
|
|
|
- {
|
|
|
- NewGame();
|
|
|
- }
|
|
|
+ public override void _Ready()
|
|
|
+ {
|
|
|
+ NewGame();
|
|
|
}
|
|
|
|
|
|
Let's also assign ``Main`` as our "Main Scene" - the one that runs automatically
|
|
@@ -1187,7 +1197,7 @@ message:
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
var hud = GetNode<HUD>("HUD");
|
|
|
- hud.UpdateScore(_score);
|
|
|
+ hud.UpdateScore(score);
|
|
|
hud.ShowMessage("Get Ready!");
|
|
|
|
|
|
In ``game_over()`` we need to call the corresponding ``HUD`` function:
|
|
@@ -1211,7 +1221,7 @@ sync with the changing score:
|
|
|
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
- GetNode<HUD>("HUD").UpdateScore(_score);
|
|
|
+ GetNode<HUD>("HUD").UpdateScore(score);
|
|
|
|
|
|
Now you're ready to play! Click the "Play the Project" button. You will
|
|
|
be asked to select a main scene, so choose ``Main.tscn``.
|
|
@@ -1231,16 +1241,17 @@ click "Groups" and you can type a new group name and click "Add".
|
|
|
.. image:: img/group_tab.png
|
|
|
|
|
|
Now all mobs will be in the "mobs" group. We can then add the following line to
|
|
|
-the ``game_over()`` function in ``Main``:
|
|
|
+the ``new_game()`` function in ``Main``:
|
|
|
|
|
|
.. tabs::
|
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
|
|
get_tree().call_group("mobs", "queue_free")
|
|
|
|
|
|
-
|
|
|
.. code-tab:: csharp
|
|
|
|
|
|
+ // Note that for calling Godot-provided methods with strings,
|
|
|
+ // we have to use the original Godot snake_case name.
|
|
|
GetTree().CallGroup("mobs", "queue_free");
|
|
|
|
|
|
The ``call_group()`` function calls the named function on every node in a group -
|