Added C# examples & info for the Your First Game example (#1297)

* Added C# examples & information for the your first game example
This commit is contained in:
Lamonte
2018-04-04 11:08:22 -05:00
committed by Max Hilbrunner
parent e5f0cb2e85
commit 753e45479d

View File

@@ -140,6 +140,9 @@ node, so we'll add a script. Click the ``Player`` node and click the
In the script settings window, you can leave the default settings alone. Just
click "Create":
.. note:: If you're creating a C# script or other languages, select the
language from the `language` drop down menu before hitting create.
.. image:: img/attach_node_window.png
.. note:: If this is your first time encountering GDScript, please read
@@ -147,28 +150,51 @@ click "Create":
Start by declaring the member variables this object will need:
::
.. tabs::
.. code-tab:: gdscript GDScript
extends Area2D
export (int) var SPEED # how fast the player will move (pixels/sec)
var screensize # size of the game window
.. code-tab:: csharp
public class Player : Area2D
{
[Export]
public int Speed = 0;
private Vector2 _screenSize;
}
Using the ``export`` keyword on the first variable ``SPEED`` allows us to
set its value in the Inspector. This can be very handy for values that you
want to be able to adjust just like a node's built-in properties. Click on
the ``Player`` node and set the speed property to ``400``.
.. warning:: If you're using C#, you need to restart godot editor temporarily to see
exported variables in the editor until it's fixed.
.. image:: img/export_variable.png
The ``_ready()`` function is called when a node enters the scene tree,
which is a good time to find the size of the game window:
::
.. tabs::
.. code-tab:: gdscript GDScript
func _ready():
screensize = get_viewport_rect().size
.. code-tab:: csharp
public override void _Ready()
{
_screenSize = GetViewport().GetSize();
}
Now we can use the ``_process()`` function to define what the player will do.
``_process()`` is called every frame, so we'll use it to update
elements of our game which we expect will change often. Here we'll make it:
@@ -188,7 +214,8 @@ You can detect whether a key is pressed using
``Input.is_action_pressed()``, which returns ``true`` if it is pressed
or ``false`` if it isn't.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _process(delta):
var velocity = Vector2() # the player's movement vector
@@ -206,6 +233,36 @@ or ``false`` if it isn't.
else:
$AnimatedSprite.stop()
.. code-tab:: csharp
public override void _Process(float delta)
{
var velocity = new Vector2();
if (Input.IsActionPressed("ui_right")) {
velocity.x += 1;
}
if (Input.IsActionPressed("ui_left")) {
velocity.x -= 1;
}
if (Input.IsActionPressed("ui_down")) {
velocity.y += 1;
}
if (Input.IsActionPressed("ui_up")) {
velocity.y -= 1;
}
var animatedSprite = (AnimatedSprite) GetNode("AnimatedSprite");
if (velocity.Length() > 0) {
velocity = velocity.Normalized() * Speed;
animatedSprite.Play();
} else {
animatedSprite.Stop();
}
}
We check each input and add/subtract from the ``velocity`` to obtain a
total direction. For example, if you hold ``right`` and ``down`` at
the same time, the resulting ``velocity`` vector will be ``(1, 1)``. In
@@ -230,14 +287,24 @@ AnimatedSprite animation.
So in the code above, ``$AnimatedSprite.play()`` is the same as ``get_node("AnimatedSprite").play()``.
Now that we have a movement direction, we can update ``Player``'s position
and use ``clamp()`` to prevent it from leaving the screen:
and use ``clamp()`` to prevent it from leaving the screen by adding the following
to the bottom of the ``_process`` function:
::
.. tabs::
.. code-tab:: gdscript GDScript
position += velocity * delta
position.x = clamp(position.x, 0, screensize.x)
position.y = clamp(position.y, 0, screensize.y)
.. code-tab:: csharp
Position += velocity * delta;
Position = new Vector2(
Mathf.Clamp(Position.x, 0, _screenSize.x),
Mathf.Clamp(Position.y, 0, _screenSize.y)
);
.. tip:: *Clamping* a value means restricting it to a given range.
@@ -258,7 +325,8 @@ property for left movement, and an "up" animation, which should be
flipped vertically with ``flip_v`` for downward movement.
Let's place this code at the end of our ``_process()`` function:
::
.. tabs::
.. code-tab:: gdscript GDScript
if velocity.x != 0:
$AnimatedSprite.animation = "right"
@@ -267,16 +335,32 @@ Let's place this code at the end of our ``_process()`` function:
elif velocity.y != 0:
$AnimatedSprite.animation = "up"
$AnimatedSprite.flip_v = velocity.y > 0
.. code-tab:: csharp
if (velocity.x != 0) {
animatedSprite.Animation = "right";
animatedSprite.FlipH = velocity.x < 0;
animatedSprite.FlipV = false;
} else if(velocity.y != 0) {
animatedSprite.Animation = "up";
animatedSprite.FlipV = velocity.y > 0;
}
Play the scene again and check that the animations are correct in each
of the directions. When you're sure the movement is working correctly,
add this line to ``_ready()`` so the player will be hidden when the game
starts:
::
.. tabs::
.. code-tab:: gdscript GDScript
hide()
.. code-tab:: csharp
Hide();
Preparing for Collisions
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -286,10 +370,16 @@ made any enemies yet! That's OK, because we're going to use Godot's
Add the following at the top of the script, after ``extends Area2d``:
::
.. tabs::
.. code-tab:: gdscript GDScript
signal hit
.. code-tab:: csharp
[Signal]
public delegate void Hit();
This defines a custom signal called "hit" that we will have our player
emit (send out) when it collides with an enemy. We will use ``Area2D`` to
detect the collision. Select the ``Player`` node and click the "Node" tab
@@ -311,13 +401,27 @@ settings - Godot will automatically create a function called
Add this code to the function:
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Player_body_entered( body ):
hide() # Player disappears after being hit
emit_signal("hit")
$CollisionShape2D.disabled = true
.. code-tab:: csharp
public void OnPlayerBodyEntered(Godot.Object body)
{
Hide();
EmitSignal("Hit");
// for the sake of this example, but it's better to create a class var
// then assign the variable inside _Ready()
var collisionShape2D = (CollisionShape2D) GetNode("CollisionShape2D");
collisionShape2D.Disabled = true;
}
.. Note:: Disabling the area's collision shape means
it won't detect collisions. By turning it off, we make
sure we don't trigger the ``hit`` signal more than once.
@@ -326,13 +430,25 @@ Add this code to the function:
The last piece for our player is to add a function we can call to reset
the player when starting a new game.
::
.. tabs::
.. code-tab:: gdscript GDScript
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
.. code-tab:: csharp
public void Start(Vector2 pos)
{
Position = pos;
Show();
var collisionShape2D = (CollisionShape2D) GetNode("CollisionShape2D");
collisionShape2D.Disabled = false;
}
Enemy Scene
-----------
@@ -388,7 +504,8 @@ Enemy Script
Add a script to the ``Mob`` and add the following member variables:
::
.. tabs::
.. code-tab:: gdscript GDScript
extends RigidBody2D
@@ -396,6 +513,19 @@ Add a script to the ``Mob`` and add the following member variables:
export (int) var MAX_SPEED # maximum speed range
var mob_types = ["walk", "swim", "fly"]
.. code-tab:: csharp
public class Mob : RigidBody2D
{
[Export]
public int MinSpeed = 150;
[Export]
public int MaxSpeed = 250;
private String[] _mobTypes = {"walk", "swim", "fly"};
}
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). Set them to ``150`` and ``250`` in the Inspector. We
@@ -405,11 +535,27 @@ we'll use to select a random one.
Now let's look at the rest of the script. In ``_ready()`` we randomly
choose one of the three animation types:
::
.. tabs::
.. code-tab:: gdscript GDScript
func _ready():
$AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
.. code-tab:: csharp
public override void _Ready()
{
var animatedSprite = (AnimatedSprite) GetNode("AnimatedSprite");
// C# doesn't implement gdscript's random methods, so we use Random
// as an alternative.
//
// Note: Never define random multiple times in real projects. Create a
// class memory and reuse it to get true random numbers.
var randomMob = new Random();
animatedSprite.Animation = _mobTypes[randomMob.Next(0, _mobTypes.Length)];
}
.. note:: You must use ``randomize()`` if you want
your sequence of "random" numbers to be different every time you run
the scene. We're going to use ``randomize()`` in our ``Main`` scene,
@@ -420,11 +566,19 @@ The last piece is to make the mobs delete themselves when they leave the
screen. Connect the ``screen_exited()`` signal of the ``Visibility``
node and add this code:
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Visibility_screen_exited():
queue_free()
.. code-tab:: csharp
public void onVisibilityScreenExited()
{
QueueFree();
}
This completes the `Mob` scene.
Main Scene
@@ -492,7 +646,8 @@ Add a script to ``Main``. At the top of the script we use
``export (PackedScene)`` to allow us to choose the Mob scene we want to
instance.
::
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
@@ -502,6 +657,30 @@ instance.
func _ready():
randomize()
.. code-tab:: csharp
public class Main : Node
{
[Export]
public PackedScene Mob;
public int Score = 0;
// note: we're going to use this many times, so instantiating it
// allows our numbers to consistently be random
private Random rand = new Random();
public override void _Ready()
{
}
// we'll use this later because c# doesn't support gdscript's randi()
private float RandRand(float min, float max)
{
return (float) (rand.NextDouble() * (max - min) + min);
}
}
Drag ``Mob.tscn`` from the "FileSystem" panel and drop it in the
``Mob`` property under the Script Variables of the ``Main`` node.
@@ -511,7 +690,8 @@ game ends. Type "game_over" in the "Method In Node" box at the bottom of the
"Connecting Signal" window. Add the following code, as well as a ``new_game``
function to set everything up for a new game:
::
.. tabs::
.. code-tab:: gdscript GDScript
func game_over():
$ScoreTimer.stop()
@@ -522,12 +702,36 @@ function to set everything up for a new game:
$Player.start($StartPosition.position)
$StartTimer.start()
.. code-tab:: csharp
public void GameOver()
{
//timers
var mobTimer = (Timer) GetNode("MobTimer");
var scoreTimer = (Timer) GetNode("ScoreTimer");
scoreTimer.Stop();
mobTimer.Stop();
}
public void NewGame()
{
Score = 0;
var player = (Player) GetNode("Player");
var startTimer = (Timer) GetNode("StartTimer");
var startPosition = (Position2D) GetNode("StartPosition");
player.Start(startPosition.Position);
startTimer.Start();
}
Now connect the ``timeout()`` signal of each of the Timer nodes.
``StartTimer`` will start the other two timers. ``ScoreTimer`` will
increment the score by 1.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_StartTimer_timeout():
$MobTimer.start()
@@ -536,6 +740,23 @@ increment the score by 1.
func _on_ScoreTimer_timeout():
score += 1
.. code-tab:: csharp
public void OnStartTimerTimeout()
{
//timers
var mobTimer = (Timer) GetNode("MobTimer");
var scoreTimer = (Timer) GetNode("ScoreTimer");
mobTimer.Start();
scoreTimer.Start();
}
public void OnScoreTimerTimeout()
{
Score += 1;
}
In ``_on_MobTimer_timeout()`` we will create a mob instance, pick a
random starting location along the ``Path2D``, and set the mob in
motion. The ``PathFollow2D`` node will automatically rotate as it
@@ -545,7 +766,8 @@ well as its position.
Note that a new instance must be added to the scene using
``add_child()``.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_MobTimer_timeout():
# choose a random location on Path2D
@@ -563,6 +785,27 @@ Note that a new instance must be added to the scene using
# choose the velocity
mob.set_linear_velocity(Vector2(rand_range(mob.MIN_SPEED, mob.MAX_SPEED), 0).rotated(direction))
.. code-tab:: csharp
public void OnMobTimerTimeout()
{
//choose random location on path2d
var mobSpawnLocation = (PathFollow2D) GetNode("MobPath/MobSpawnLocation");
mobSpawnLocation.SetOffset(rand.Next());
//set direction
var direction = mobSpawnLocation.Rotation + Mathf.PI/2;
direction += RandRand(-Mathf.PI/4, Mathf.PI/4);
//create mob instance and add it to scene
var mobInstance = (RigidBody2D) Mob.Instance();
mobInstance.Position = mobSpawnLocation.Position;
mobInstance.Rotation = direction;
mobInstance.SetLinearVelocity(new Vector2(RandRand(150f, 250f), 0).Rotated(direction));
AddChild(mobInstance);
}
.. important:: In functions requiring angles, GDScript uses *radians*,
not degrees. If you're more comfortable working with
degrees, you'll need to use the ``deg2rad()`` and
@@ -669,27 +912,50 @@ the three ``Control`` nodes:
Now add this script to ``HUD``:
::
.. tabs::
.. code-tab:: gdscript GDScript
extends CanvasLayer
signal start_game
.. code-tab:: csharp
public class HUD : CanvasLayer
{
[Signal]
public delegate void StartGame();
}
The ``start_game`` signal tells the ``Main`` node that the button
has been pressed.
::
.. tabs::
.. code-tab:: gdscript GDScript
func show_message(text):
$MessageLabel.text = text
$MessageLabel.show()
$MessageTimer.start()
.. code-tab:: csharp
public void ShowMessage(string text)
{
var messageTimer = (Timer) GetNode("MessageTimer");
var messageLabel = (Label) GetNode("MessageLabel");
messageLabel.Text = text;
messageLabel.Show();
messageTimer.Start();
}
This function is called when we want to display a message
temporarily, such as "Get Ready". On the ``MessageTimer``, set the
``Wait Time`` to ``2`` and set the ``One Shot`` property to "On".
::
.. tabs::
.. code-tab:: gdscript GDScript
func show_game_over():
show_message("Game Over")
@@ -698,21 +964,48 @@ temporarily, such as "Get Ready". On the ``MessageTimer``, set the
$MessageLabel.text = "Dodge the\nCreeps!"
$MessageLabel.show()
.. code-tab:: csharp
async public void ShowGameOver()
{
ShowMessage("Game Over");
var startButton = (Button) GetNode("StartButton");
var messageTimer = (Timer) GetNode("MessageTimer");
var messageLabel = (Label) GetNode("MessageLabel");
//work around for gdscript's yield
await Task.Delay((int) messageTimer.WaitTime * 1000);
messageLabel.Text = "Dodge the\nCreeps!";
messageLabel.Show();
startButton.Show();
}
This function is called when the player loses. It will show "Game
Over" for 2 seconds, then return to the title screen and show the
"Start" button.
::
.. tabs::
.. code-tab:: gdscript GDScript
func update_score(score):
$ScoreLabel.text = str(score)
.. code-tab:: csharp
public void UpdateScore(int score)
{
var scoreLabel = (Label) GetNode("ScoreLabel");
scoreLabel.Text = score.ToString();
}
This function is called in ``Main`` whenever the score changes.
Connect the ``timeout()`` signal of ``MessageTimer`` and the
``pressed()`` signal of ``StartButton``.
::
.. tabs::
.. code-tab:: gdscript GDScript
func _on_StartButton_pressed():
$StartButton.hide()
@@ -721,6 +1014,22 @@ Connect the ``timeout()`` signal of ``MessageTimer`` and the
func _on_MessageTimer_timeout():
$MessageLabel.hide()
.. code-tab:: csharp
public void OnStartButtonPressed()
{
var startButton = (Button) GetNode("StartButton");
startButton.Hide();
EmitSignal("StartGame");
}
public void OnMessageTimerTimeout()
{
var messageLabel = (Label) GetNode("MessageLabel");
messageLabel.Hide();
}
Connecting HUD to Main
~~~~~~~~~~~~~~~~~~~~~~
@@ -740,24 +1049,43 @@ In the Node tab, connect the HUD's ``start_game`` signal to the
In ``new_game()``, update the score display and show the "Get Ready"
message:
::
.. tabs::
.. code-tab:: gdscript GDScript
$HUD.update_score(score)
$HUD.show_message("Get Ready")
.. code-tab:: csharp
var hud = (HUD) GetNode("HUD");
hud.UpdateScore(Score);
hud.ShowMessage("Get Ready!");
In ``game_over()`` we need to call the corresponding ``HUD`` function:
::
.. tabs::
.. code-tab:: gdscript GDScript
$HUD.show_game_over()
.. code-tab:: csharp
var hud = (HUD) GetNode("HUD");
hud.ShowGameOver();
Finally, add this to ``_on_ScoreTimer_timeout()`` to keep the display in
sync with the changing score:
::
.. tabs::
.. code-tab:: gdscript GDScript
$HUD.update_score(score)
.. code-tab:: csharp
var hud = (HUD) 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``.