Ensimmäinen pelisi

Yleiskatsaus

Tämä opas opastaa sinut ensimmäisen Godot-projektisi tekemisen läpi. Opit kuinka Godotin editori toimii, kuinka jäsentää projekti ja kuinka rakentaa 2D-peli.

Muista

Tämä projekti on johdanto Godotin pelimoottoriin. Siinä oletetaan, että sinulla on jo jotain ohjelmointikokemusta. Jos ohjelmointi on sinulle kokonaan uutta, sinun tulisi aloittaa tästä: Skriptaus.

Pelin nimi on "Dodge the Creeps!". Pelihahmosi täytyy liikkua ja vältellä vihollisia niin kauan kuin mahdollista. Tässä on ennakkokuva lopputuloksesta:

../../_images/dodge_preview.gif

Why 2D? 3D games are much more complex than 2D ones. You should stick to 2D until you have a good understanding of the game development process and how to use Godot.

Projektin järjestely

Launch Godot and create a new project. Then, download dodge_assets.zip. This contains the images and sounds you'll be using to make the game. Unzip these files in your project folder.

Muista

For this tutorial, we will assume you are familiar with the Godot editor. If you haven't read Skenet ja solmut, do so now for an explanation of setting up a project and using the editor.

This game is designed for portrait mode, so we need to adjust the size of the game window. Click on Project -> Project Settings -> Display -> Window and set "Width" to 480 and "Height" to 720.

Also in this section, under the "Stretch" options, set Mode to "2d" and Aspect to "keep". This ensures that the game scales consistently on different sized screens.

Projektin järjestäminen

In this project, we will make 3 independent scenes: Player, Mob, and HUD, which we will combine into the game's Main scene. In a larger project, it might be useful to create folders to hold the various scenes and their scripts, but for this relatively small game, you can save your scenes and scripts in the project's root folder, identified by res://. You can see your project folders in the FileSystem Dock in the lower left corner:

../../_images/filesystem_dock.png

Player-skene

Ensimmäinen skene, jonka teemme, määrittelee Player objektin. Yksi etu erillisen pelaajaskenen luomisesta on, että voimme testata sitä erillisesti, jo ennen kuin olemme luoneet pelin muut osat.

Solmurakenne

To begin, we need to choose a root node for the player object. As a general rule, a scene's root node should reflect the object's desired functionality - what the object is. Click the "Other Node" button and add an Area2D node to the scene.

../../_images/add_node.png

Godot näyttää varoitus kuvakkeen solmun vieressä skenepuussa. Voit sivuuttaa sen nyt. Me huomioimme sen myöhemmin.

With Area2D we can detect objects that overlap or run into the player. Change the node's name to Player by double-clicking on it. Now that we've set the scene's root node, we can add additional nodes to give it more functionality.

Ennen kuin lisäämme Player solmulle yhtään alisolmuja, haluamme pitää huolen ettemme vahingossa siirrä niitä tai muuta niiden kokoa napsauttamalla niitä. Valitse solmu ja napsauta lukon vasemmalla puolella olevaa kuvaketta; sen työkaluvihjeessä lukee "Varmistaa, ettei objektin alisolmuja voi valita."

../../_images/lock_children.png

Save the scene. Click Scene -> Save, or press Ctrl + S on Windows/Linux or Cmd + S on macOS.

Muista

Tässä projektissa noudatamme Godotin nimeämiskäytäntöjä.

  • GDScript: Luokat (solmut) käyttävät PascalCase tyyliä, muuttujat ja funktiot käyttävät snake_case tyyliä, ja vakiot käyttävät ALL_CAPS tyyliä (katso GDScript style guide).
  • C#: Luokat, vientimuuttujat ja metodit käyttävät PascalCase tyyliä, yksityiset kentät käyttävät _camelCase tyyliä, paikalliset muuttujat ja parametrit käyttävät camelCase tyyliä (katso C# style guide). Ole huolellinen ja kirjoita metodien nimet tarkasti kytkiessäsi signaaleja.

Spriten animaatio

Click on the Player node and add an AnimatedSprite node as a child. The AnimatedSprite will handle the appearance and animations for our player. Notice that there is a warning symbol next to the node. An AnimatedSprite requires a SpriteFrames resource, which is a list of the animations it can display. To create one, find the Frames property in the Inspector and click "[empty]" -> "New SpriteFrames". Click again to open the "SpriteFrames" panel:

../../_images/spriteframes_panel.png

On the left is a list of animations. Click the "default" one and rename it to "walk". Then click the "New Animation" button to create a second animation named "up". Find the player images in the "FileSystem" tab - they're in the art folder you unzipped earlier. Drag the two images for each animation, named playerGrey_up[1/2] and playerGrey_walk[1/2], into the "Animation Frames" side of the panel for the corresponding animation:

../../_images/spriteframes_panel2.png

Pelaajakuvat on hieman liian suuria peli-ikkunalle, joten meidän täytyy pienentää niitä. Napsauta AnimatedSprite solmua ja aseta Scale ominaisuus arvoon (0.5, 0.5). Löydät sen Tarkastelijasta Node2D otsikon alta.

../../_images/player_scale.png

Lisää lopuksi CollisionShape2D Player solmun alle. Tämä määrittelee pelaajan "osumalaatikon" tai törmäysalueen rajat. Tälle hahmolle sopii parhaiten CapsuleShape2D solmu, joten napsauta Tarkastelijassa "Shape" ominaisuuden vierestä "[empty]" -> "New CapsuleShape2D". Käyttäen kahta kahvaa, venytä muoto kattamaan koko sprite:

../../_images/player_coll_shape.png

Kun olet valmis, Player skenesi pitäisi näyttää tältä:

../../_images/player_scene_nodes.png

Make sure to save the scene again after these changes.

Pelaajan liikuttaminen

Now we need to add some functionality that we can't get from a built-in node, so we'll add a script. Click the Player node and click the "Attach Script" button:

../../_images/add_script_button.png

Skriptin asetukset -ikkunassa voit antaa oletusasetuksien olla sellaisenaan. Napsauta vain "Luo":

Muista

Jos olet luomassa C#-skriptiä tai muun kielistä skriptiä, valitse kieli kieli pudotusvalikosta ennen kuin huitaiset luontinappia.

../../_images/attach_node_window.png

Muista

Jos törmäät nyt ensi kertaa GDScriptiin, ole hyvä ja lue Skriptaus ennen kuin jatkat.

Aloita luomalla ne jäsenmuuttujat, jotka tämä objekti tulee tarvitsemaan:

extends Area2D

export var speed = 400  # How fast the player will move (pixels/sec).
var screen_size  # Size of the game window.
public class Player : Area2D
{
    [Export]
    public int Speed = 400; // How fast the player will move (pixels/sec).

    private Vector2 _screenSize; // Size of the game window.
}

Käyttäen export avainsanaa ensimmäiselle muuttujalle speed antaa meidän asettaa sen arvon Tarkastelijassa. Se voi olla kätevää arvoille, joita haluat säätää kuin ne olisivat solmun valmiita ominaisuuksia. Napsauta Player solmua ja aseta nopeusominaisuudeksi 400.

Varoitus

If you're using C#, you need to (re)build the project assemblies whenever you want to see new export variables or signals. This build can be manually triggered by clicking the word "Mono" at the bottom of the editor window to reveal the Mono Panel, then clicking the "Build Project" button.

../../_images/export_variable.png

_ready() funktiota kutsutaan, kun solmu lisätään skenepuuhun, mikä on hyvä hetki hakea peli-ikkunan koko:

func _ready():
    screen_size = get_viewport_rect().size
public override void _Ready()
{
    _screenSize = GetViewport().Size;
}

Nyt voimme käyttää _process() funktiota määrittelemään mitä pelaaja tekee. _process() funktiota kutsutaan jokaisella ruudunpäivityksellä, joten käytämme sitä päivittämään pelimme niitä osasia joiden odotamme muuttuvan usein. Pelaajan osalta teemme siinä seuraavaa:

  • Tarkista syöte.
  • Liiku annettuun suuntaan.
  • Toista asiaankuuluva animaatio.

Ensin meidän täytyy tarkistaa syöte - painaako pelaaja jotain näppäintä? Tässä pelissä meillä on neljän suunnan syötteet tarkistettavana. Syötetoiminnot on määritelty Projektin asetuksissa kohdassa "Input Map". Voit määritellä omia tapahtumia ja asettaa niille eri näppäimiä, hiiritapahtumia ja muita syötteitä. Tässä esimerkissä käytämme oletustapahtumia, jotka on asetettu näppäimistön nuolinäppäimille.

You can detect whether a key is pressed using Input.is_action_pressed(), which returns true if it's pressed or false if it isn't.

func _process(delta):
    var velocity = Vector2()  # The player's movement vector.
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
    if Input.is_action_pressed("ui_down"):
        velocity.y += 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()
public override void _Process(float delta)
{
    var velocity = new Vector2(); // The player's movement vector.

    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 = GetNode<AnimatedSprite>("AnimatedSprite");

    if (velocity.Length() > 0)
    {
        velocity = velocity.Normalized() * Speed;
        animatedSprite.Play();
    }
    else
    {
        animatedSprite.Stop();
    }
}

We start by setting the velocity to (0, 0) - by default, the player should not be moving. Then 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 this case, since we're adding a horizontal and a vertical movement, the player would move faster diagonally than if it just moved horizontally.

We can prevent that if we normalize the velocity, which means we set its length to 1, then multiply by the desired speed. This means no more fast diagonal movement.

Vihje

Jos et ole koskaan käyttänyt vektorimatematiikkaa, tai tarvitset kertausta, voit katsoa selityksen Godotin vektorien käytöstä: Vector math. Se on hyvä osata, mutta ei ole välttämätön oppaan loppuosan kannalta.

We also check whether the player is moving so we can call play() or stop() on the AnimatedSprite.

$ on lyhenne get_node() syntaksille. Toisin sanoen, yllä olevassa koodissa $AnimatedSprite.play() on sama kuin get_node("AnimatedSprite").play().

Vihje

GDScriptissä, $ palauttaa solmun, joka löytyy suhteellisesta polusta nykyiseen solmuun nähden, tai palauttaa null jos solmua ei löydy. Koska AnimatedSprite on nykyisen solmun alisolmu, voimme käyttää $AnimatedSprite syntaksia.

Now that we have a movement direction, we can update the player's position. We can also use clamp() to prevent it from leaving the screen. Clamping a value means restricting it to a given range. Add the following to the bottom of the _process function (make sure it's not indented under the else):

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;
Position = new Vector2(
    x: Mathf.Clamp(Position.x, 0, _screenSize.x),
    y: Mathf.Clamp(Position.y, 0, _screenSize.y)
);

Vihje

delta`parametri `_process() funktiossa viittaa ruudun (animaatiossa) pituuteen -aikaan joka kului edellisen ruudun suorittamiseen. Tätä arvoa käyttämällä varmistutaan että liike on säännönmukainen vaikka ruudun päivitysnopeus muuttuisi.

Napsauta "Pelaa skeneä" (F6) ja varmista, että voit liikuttaa pelaajaa ympäri ruutua kaikkiin suuntiin.

Varoitus

If you get an error in the "Debugger" panel that says

Attempt to call function 'play' in base 'null instance' on a null instance

this likely means you spelled the name of the AnimatedSprite node wrong. Node names are case-sensitive and $NodeName must match the name you see in the scene tree.

Animaatioiden valinta

Now that the player can move, we need to change which animation the AnimatedSprite is playing based on its direction. We have the "walk" animation, which shows the player walking to the right. This animation should be flipped horizontally using the flip_h property for left movement. We also have the "up" animation, which should be flipped vertically with flip_v for downward movement. Let's place this code at the end of the _process() function:

if velocity.x != 0:
    $AnimatedSprite.animation = "walk"
    $AnimatedSprite.flip_v = false
    # See the note below about boolean assignment
    $AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
    $AnimatedSprite.animation = "up"
    $AnimatedSprite.flip_v = velocity.y > 0
if (velocity.x != 0)
{
    animatedSprite.Animation = "walk";
    animatedSprite.FlipV = false;
    // See the note below about boolean assignment
    animatedSprite.FlipH = velocity.x < 0;
}
else if (velocity.y != 0)
{
    animatedSprite.Animation = "up";
    animatedSprite.FlipV = velocity.y > 0;
}

Muista

The boolean assignments in the code above are a common shorthand for programmers. Since we're doing a comparison test (boolean) and also assigning a boolean value, we can do both at the same time. Consider this code versus the one-line boolean assignment above:

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false
if (velocity.x < 0)
{
    animatedSprite.FlipH = true;
}
else
{
    animatedSprite.FlipH = false;
}

Play the scene again and check that the animations are correct in each of the directions.

Vihje

A common mistake here is to type the names of the animations wrong. The animation names in the SpriteFrames panel must match what you type in the code. If you named the animation "Walk", you must also use a capital "W" in the code.

When you're sure the movement is working correctly, add this line to _ready(), so the player will be hidden when the game starts:

hide()
Hide();

Törmäyksiin valmistautuminen

Haluamme, että Player havaitsee milloin vihollinen on törmännyt siihen, mutta emme ole tehneet vielä vihollisia! Se on OK, koska aiomme laittaa sen toimimaan Godotin signal toiminnallisuudella.

Add the following at the top of the script, after extends Area2D:

signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.

[Signal]
public delegate void Hit();

Tämä määrittelee mukautetun signaalin nimeltä "hit", jonka laitamme pelaajamme lähettämään, kun se törmää viholliseen. Käytämme Area2D solmua tunnistamaan törmäyksen. Valitse Player solmu ja napsauta "Solmu" välilehteä Tarkastelijan vieressä nähdäksesi listan signaaleista, joita pelaaja voi lähettää:

../../_images/player_signals.png

Notice our custom "hit" signal is there as well! Since our enemies are going to be RigidBody2D nodes, we want the body_entered(body: Node) signal. This signal will be emitted when a body contacts the player. Click "Connect.." and the "Connect a Signal" window appears. We don't need to change any of these settings so click "Connect" again. Godot will automatically create a function in your player's script.

../../_images/player_signal_connection.png

Note the green icon indicating that a signal is connected to this function. Add this code to the function:

func _on_Player_body_entered(body):
    hide()  # Player disappears after being hit.
    emit_signal("hit")
    $CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
    Hide(); // Player disappears after being hit.
    EmitSignal("Hit");
    GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}

Joka kerta kun vihollinen osuu pelaajaan, signaali välitetään. Meidän täytyy poistaa käytöstä pelaajan törmäys ettei se laukaise enää hit signaalia enempää kuin kerran.

Muista

Disabling the area's collision shape can cause an error if it happens in the middle of the engine's collision processing. Using set_deferred() tells Godot to wait to disable the shape until it's safe to do so.

The last piece is to add a function we can call to reset the player when starting a new game.

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false
public void Start(Vector2 pos)
{
    Position = pos;
    Show();
    GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}

Vihollisen skene

Now it's time to make the enemies our player will have to dodge. Their behavior will not be very complex: mobs will spawn randomly at the edges of the screen, choose a random direction, and move in a straight line.

We'll create a Mob scene, which we can then instance to create any number of independent mobs in the game.

Muista

Katso Ilmentymien luonti oppiaksesi enemmän ilmentymistä.

Solmun järjestely

Click Scene -> New Scene and add the following nodes:

Älä unohda asettaa alisolmuja niin, ettei niitä voi valita, kuten teit Player skenen kanssa.

RigidBody2D ominaisuuksissa, aseta Gravity Scale arvoon 0, jotta vihulainen ei putoa alaspäin. Lisäksi, PhysicsBody2D osiossa, napsauta Mask ominaisuutta ja poista merkintä ensimmäisestä laatikosta. Tämä varmistaa, etteivät viholliset törmäile toisiinsa.

../../_images/set_collision_mask.png

Set up the AnimatedSprite like you did for the player. This time, we have 3 animations: fly, swim, and walk. There are two images for each animation in the art folder.

Adjust the "Speed (FPS)" to 3 for all animations.

../../_images/mob_animations.gif

Set the Playing property in the Inspector to “On”.

We'll select one of these animations randomly so that the mobs will have some variety.

Kuten pelaajan kuvat, nämä vihollisten kuvatkin täytyy pienentää. Aseta AnimatedSprite solmun Scale ominaisuus arvoon (0.75, 0.75).

As in the Player scene, add a CapsuleShape2D for the collision. To align the shape with the image, you'll need to set the Rotation Degrees property to 90 (under "Transform" in the Inspector).

Save the scene.

Vihollisen skripti

Lisää Mob solmulle skripti ja lisää seuraavat jäsenmuuttujat:

extends RigidBody2D

export var min_speed = 150  # Minimum speed range.
export var max_speed = 250  # Maximum speed range.
public class Mob : RigidBody2D
{
    // Don't forget to rebuild the project so the editor knows about the new export variables.

    [Export]
    public int MinSpeed = 150; // Minimum speed range.

    [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).

Katsokaamme nyt skriptin jäljellä olevaa osaa. _ready() funktiossa valitsemme yhden kolmesta animaatiotyypistä:

func _ready():
    var mob_types = $AnimatedSprite.frames.get_animation_names()
    $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
// 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)];
}

First, we get the list of animation names from the AnimatedSprite's frames property. This returns an Array containing all three animation names: ["walk", "swim", "fly"].

We then need to pick a random number between 0 and 2 to select one of these names from the list (array indices start at 0). randi() % n selects a random integer between 0 and n-1.

Muista

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, so we won't need it here.

The last piece is to make the mobs delete themselves when they leave the screen. Connect the screen_exited() signal of the VisibilityNotifier2D node and add this code:

func _on_VisibilityNotifier2D_screen_exited():
    queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
    QueueFree();
}

Tämä päättää Mob skenen.

Pääskene

Nyt on aika saattaa kaikki yhteen. Luo uusi skene ja lisää siihen Node solmu nimeltä Main. Napsauta "Ilmentymä" painiketta ja valitse tallentamasi Player.tscn.

../../_images/instance_scene.png

Lisää nyt seuraavat solmut Main solmun alisolmuiksi ja nimeä ne kuten esitetty (arvot ovat sekunteina):

  • Timer (nimeltään MobTimer) - ohjaamaan kuinka usein vihulaiset ilmestyvät
  • Timer (nimeltään ScoreTimer) - kasvattamaan pistelaskuria joka sekunti
  • Timer (nimeltään StartTimer) - antamaan viive ennen aloittamista
  • Position2D (nimeltään StartPosition) - merkkaamaan pelaajan aloituspaikkaa

Aseta kunkin Timer solmun Wait Time ominaisuus seuraavasti:

  • MobTimer: 0.5
  • ScoreTimer: 1
  • StartTimer: 2

Lisäksi, aseta StartTimer solmun One Shot ominaisuus arvoon "On", ja aseta StartPosition solmun Position ominaisuudeksi (240, 450).

Vihollisten lisääminen

Main solmu tuottaa uusia vihollisia ja haluamme niiden ilmestyvän satunnaiseen paikkaan ruudun reunalla. Lisää Main solmun alisolmuksi Path2D solmu nimeltä MobPath. Kun valitset Path2D solmun, näet joitakin uusia painikkeita editorin ylälaidassa:

../../_images/path2d_buttons.png

Select the middle one ("Add Point") and draw the path by clicking to add the points at the corners shown. To have the points snap to the grid, make sure "Use Grid Snap" is selected. This option can be found to the left of the "Lock" button, appearing as a magnet next to some intersecting lines.

../../_images/grid_snap_button.png

Tärkeä

Piirrä polku myötäpäiväisessä järjestyksessä, tai vihulaisesi lisääntyvät suunnaten ulospäin eikä sisäänpäin!

../../_images/draw_path2d.gif

Kuvan pisteen 4 lisäyksen jälkeen, napsauta "Sulje käyrä" painiketta ja käyrästäsi tulee täysi.

Nyt kun polku on määritetty, lisää PathFollow2D solmu MobPath solmun alle ja nimeä se MobSpawnLocation. Tämä solmu kiertyy ja seuraa polkua automaattisesti edetessään, joten voimme käyttää sitä valitsemaan satunnaisen sijainnin ja suunnan polulta.

Your scene should look like this:

../../_images/main_scene_nodes.png

Pääskripti

Lisää skripti Main solmulle. Skriptin alussa käytämme export (PackedScene) koodia, jotta voimme valita mistä Mob skenestä haluamme luoda ilmentymän.

extends Node

export (PackedScene) var Mob
var score

func _ready():
    randomize()
public class Main : Node
{
    // Don't forget to rebuild the project so the editor knows about the new export variable.

    [Export]
    public PackedScene Mob;

    private int _score;

    // We use 'System.Random' as an alternative to GDScript's random methods.
    private Random _random = new Random();

    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;
    }
}

Click the Main node and you will see the Mob property in the Inspector under "Script Variables".

You can assign this property's value in two ways:

  • Drag Mob.tscn from the "FileSystem" panel and drop it in the Mob property .
  • Click the down arrow next to "[empty]" and choose "Load". Select Mob.tscn.

Next, select the Player node in the Scene dock, and access the Node dock on the sidebar. Make sure to have the Signals tab selected in the Node dock.

You should see a list of the signals for the Player node. Find and double-click the hit signal in the list (or right-click it and select "Connect..."). This will open the signal connection dialog. We want to make a new function named game_over, which will handle what needs to happen when a game ends. Type "game_over" in the "Receiver Method" box at the bottom of the signal connection dialog and click "Connect". Add the following code to the new function, as well as a new_game function that will set everything up for a new game:

func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()
public void GameOver()
{
    GetNode<Timer>("MobTimer").Stop();
    GetNode<Timer>("ScoreTimer").Stop();
}

public void NewGame()
{
    _score = 0;

    var player = GetNode<Player>("Player");
    var startPosition = GetNode<Position2D>("StartPosition");
    player.Start(startPosition.Position);

    GetNode<Timer>("StartTimer").Start();
}

Now connect the timeout() signal of each of the Timer nodes (StartTimer, ScoreTimer , and MobTimer) to the main script. StartTimer will start the other two timers. ScoreTimer will increment the score by 1.

func _on_StartTimer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()

func _on_ScoreTimer_timeout():
    score += 1
public void OnStartTimerTimeout()
{
    GetNode<Timer>("MobTimer").Start();
    GetNode<Timer>("ScoreTimer").Start();
}

public void OnScoreTimerTimeout()
{
    _score++;
}

_on_MobTimer_timeout() funktiossa luomme vihollisilmentymän, valitsemme satunnaisen aloituspaikan Path2D varrelta, ja sysäämme vihollisen liikkeelle. PathFollow2D solmu kiertyy automaattisesti seuratessaan polkua, joten hyödynnämme sitä valitsemaan sekä vihollisen suunnan että sijainnin.

Huomaa, että uusi ilmentymä täytyy lisätä skeneen käyttämällä add_child() funktiota.

func _on_MobTimer_timeout():
    # Choose a random location on Path2D.
    $MobPath/MobSpawnLocation.offset = randi()
    # Create a Mob instance and add it to the scene.
    var mob = Mob.instance()
    add_child(mob)
    # Set the mob's direction perpendicular to the path direction.
    var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
    # Set the mob's position to a random location.
    mob.position = $MobPath/MobSpawnLocation.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)
public void OnMobTimerTimeout()
{
    // Choose a random location on Path2D.
    var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
    mobSpawnLocation.Offset = _random.Next();

    // Create a Mob instance and add it to the scene.
    var mobInstance = (RigidBody2D)Mob.Instance();
    AddChild(mobInstance);

    // 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;

    // Add some randomness to the direction.
    direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
    mobInstance.Rotation = direction;

    // Choose the velocity.
    mobInstance.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
}

Tärkeä

Why PI? 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 rad2deg() functions to convert between the two.

Testing the scene

Let's test the scene to make sure everything is working. Add this to _ready():

func _ready():
    randomize()
    new_game()
    public override void _Ready()
    {
        NewGame();
    }
}

Let's also assign Main as our "Main Scene" - the one that runs automatically when the game launches. Press the "Play" button and select Main.tscn when prompted.

You should be able to move the player around, see mobs spawning, and see the player disappear when hit by a mob.

When you're sure everything is working, remove the call to new_game() from _ready().

Heijastusnäyttö (HUD)

Pelimme tarvitsema viimeinen osa on käyttöliittymä: sillä esitetään sellaisia juttuja kuten pistemäärä, "game over"-viesti ja "aloita alusta"-painike. Luo uusi skene ja lisää CanvasLayer solmu nimeltä HUD. "HUD" tulee sanoista "heads-up display" (suom. heijastusnäyttö). Se on informaatiota sisältävä näyttö, joka piirretään osittaiseksi peitteeksi pelinäkymän päälle.

CanvasLayer solmu antaa meidän piirtää käyttöliittymäelementtimme muun pelin yläpuolella olevalle kerrokselle, niin että sen esittämä tieto ei jää pelielementtien, kuten pelaaja ja vihollinen, alle.

The HUD needs to display the following information:

  • Pistemäärä, joka muuttuu ScoreTimer solmun toimesta.
  • Viesti, kuten "Game Over" tai "Get Ready!"
  • "Start"-painike pelin aloittamiseksi.

Pohjasolmu käyttöliittymäelementeille on Control. Luodaksemme käyttöliittymämme käytämme kahdentyyppisiä Control solmuja: Label ja Button.

Luo seuraavat HUD solmun alle:

  • Label nimeltä ScoreLabel.
  • Label named Message.
  • Button nimeltä StartButton.
  • Timer nimeltä MessageTimer.

Click on the ScoreLabel and type a number into the Text field in the Inspector. The default font for Control nodes is small and doesn't scale well. There is a font file included in the game assets called "Xolonium-Regular.ttf". To use this font, do the following:

  1. "Custom Fonts" alla, valitse "New DynamicFont"
../../_images/custom_font1.png
  1. Napsauta lisäämääsi "DynamicFont" kirjasinta, valitse "Font Data" alta "Load" ja valitse "Xolonium-Regular.ttf" tiedosto. Sinun täytyy myös asettaa kirjasimen koko Size ominaisuudesta. Asetus 64 toimii hyvin.
../../_images/custom_font2.png

Once you've done this on the ScoreLabel, you can click the down arrow next to the DynamicFont property and choose "Copy", then "Paste" it in the same place on the other two Control nodes.

Muista

Ankkurit ja marginaalit: Control solmuilla täytyy olla sijainti ja koko, mutta niillä on myös ankkurit ja marginaalit. Ankkurit määrittävät alkupisteen – viitepisteen solmun reunoille. Marginaalit päivittyvät automaattisesti, kun liikutat Control-solmua tai muutat sen kokoa. Ne edustavat etäisyyttä Control-solmun reunoista sen ankkuriin. Katso Design interfaces with the Control nodes tarkempia tietoja varten.

Arrange the nodes as shown below. Click the "Layout" button to set a Control node's layout:

../../_images/ui_anchor.png

Voit kiskoa solmuja asettaaksesi ne manuaalisesti, tai tarkempaa sijoittelua varten, voit käyttää seuraavia asetuksia:

ScoreLabel

  • Layout : "Top Wide"
  • Text : 0
  • Align : "Center"

Message

  • Layout : "HCenter Wide"
  • Text : Dodge the Creeps!
  • Align : "Center"
  • Autowrap : "On"

StartButton

  • Text : Start
  • Layout : "Center Bottom"
  • Margin :
    • Top: -200
    • Bottom: -100

On the MessageTimer, set the Wait Time to 2 and set the One Shot property to "On".

Lisää nyt tämä skripti HUD solmulle:

extends CanvasLayer

signal start_game
public class HUD : CanvasLayer
{
    // Don't forget to rebuild the project so the editor knows about the new signal.

    [Signal]
    public delegate void StartGame();
}

start_game signaali kertoo Main solmulle, että painiketta on painettu.

func show_message(text):
    $Message.text = text
    $Message.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var message = GetNode<Label>("Message");
    message.Text = text;
    message.Show();

    GetNode<Timer>("MessageTimer").Start();
}

This function is called when we want to display a message temporarily, such as "Get Ready".

func show_game_over():
    show_message("Game Over")
    # Wait until the MessageTimer has counted down.
    yield($MessageTimer, "timeout")

    $Message.text = "Dodge the\nCreeps!"
    $Message.show()
    # Make a one-shot timer and wait for it to finish.
    yield(get_tree().create_timer(1), "timeout")
    $StartButton.show()
async public void ShowGameOver()
{
    ShowMessage("Game Over");

    var messageTimer = GetNode<Timer>("MessageTimer");
    await ToSignal(messageTimer, "timeout");

    var message = GetNode<Label>("Message");
    message.Text = "Dodge the\nCreeps!";
    message.Show();

    await ToSignal(GetTree().CreateTimer(1), "timeout");
    GetNode<Button>("StartButton").Show();
}

Tätä funktiota kutsutaan kun pelaaja häviää. Se näyttää "Game Over" kahden sekunnin ajan, ja palaa sitten alkuruutuun ja näyttää "Start" painikkeen lyhyen tauon jälkeen.

Muista

Kun sinun täytyy pysäyttää koodi hetkeksi vaihtoehto ajastimelle on käyttää skenepuun create_timer() funktiota. Tämä voi olla todella hyödyllistä luomaan viiveitä kuten ylläolevassa koodissa, missä me haluamme odottaa hetken ennenkuin näytämme "Start" painikkeen.

func update_score(score):
    $ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
    GetNode<Label>("ScoreLabel").Text = score.ToString();
}

Tätä funktiota kutsutaan Main solmussa aina kun pistemäärä muuttuu.

Connect the timeout() signal of MessageTimer and the pressed() signal of StartButton and add the following code to the new functions:

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $Message.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

public void OnMessageTimerTimeout()
{
    GetNode<Label>("Message").Hide();
}

HUD- ja Main-skenejen liittäminen

Now that we're done creating the HUD scene, go back to Main. Instance the HUD scene in Main like you did the Player scene. The scene tree should look like this, so make sure you didn't miss anything:

../../_images/completed_main_scene.png

Nyt meidän täytyy liittää HUD toiminnallisuus Main skriptiimme. Tämä edellyttää muutamia lisäyksiä Main skeneen:

In the Node tab, connect the HUD's start_game signal to the new_game() function of the Main node by typing "new_game" in the "Receiver Method" in the "Connect a Signal" window. Verify that the green connection icon now appears next to func new_game() in the script.

new_game() funktiossa, päivitä pistenäyttö ja näytä "Get Ready" viesti:

$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(_score);
hud.ShowMessage("Get Ready!");

game_over() funktiossa meidän täytyy kutsua vastaavaa HUD funktiota:

$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();

Lopuksi, lisää tämä _on_ScoreTimer_timeout() funktioon pitääksesi näytön synkassa muuttuvan pistemäärän kanssa:

$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(_score);

Nyt olet valmis pelaamaan! Napsauta "Pelaa" painiketta. Sinua pyydetään valitsemaan pääskene, joten valitse Main.tscn.

Vanhojen creepsien poisto

If you play until "Game Over" and then start a new game right away, the creeps from the previous game may still be on the screen. It would be better if they all disappeared at the start of a new game. We just need a way to tell all the mobs to remove themselves. We can do this with the "group" feature.

In the Mob scene, select the root node and click the "Node" tab next to the Inspector (the same place where you find the node's signals). Next to "Signals", click "Groups" and you can type a new group name and click "Add".

../../_images/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:

get_tree().call_group("mobs", "queue_free")
GetTree().CallGroup("mobs", "queue_free");

The call_group() function calls the named function on every node in a group - in this case we are telling every mob to delete itself.

Viimeistely

Olemme saaneet valmiiksi kaiken toiminnallisuuden pelissämme. Alla on joitakin jäljellä olevia toimenpiteitä pelikokemuksen mehevöittämiseksi. Voit vapaasti laajentaa pelattavuutta omilla ideoillasi.

Tausta

The default gray background is not very appealing, so let's change its color. One way to do this is to use a ColorRect node. Make it the first node under Main so that it will be drawn behind the other nodes. ColorRect only has one property: Color. Choose a color you like and select "Layout" -> "Full Rect" so that it covers the screen.

You could also add a background image, if you have one, by using a TextureRect node instead.

Ääniefektit

Äänet ja musiikki voivat olla kaikkein tehokkain tapa lisätä pelikokemuksen miellekkyyttä. Pelisi asset-kansiossa on kaksi äänitiedostoa: "House In a Forest Loop.ogg" taustamusiikille ja "gameover.wav" sitä varten, kun pelaaja häviää.

Lisää kaksi AudioStreamPlayer solmua Main solmun alle. Anna ensimmäiselle nimeksi Music ja toiselle DeathSound. Napsauta kummallekin Stream ominaisuutta, valitse "Lataa" ja valitse nimeä vastaava äänitiedosto.

Soittaaksesi musiikkia, lisää $Music.play() new_game() funktioon ja $Music.stop() game_over() funktioon.

Lopuksi, lisää $DeathSound.play() game_over() funktioon.

Näppäimistön pikanäppäin

Since the game is played with keyboard controls, it would be convenient if we could also start the game by pressing a key on the keyboard. We can do this with the "Shortcut" property of the Button node.

In the HUD scene, select the StartButton and find its Shortcut property in the Inspector. Select "New Shortcut" and click on the "Shortcut" item. A second Shortcut property will appear. Select "New InputEventAction" and click the new "InputEventAction". Finally, in the Action property, type the name ui_select. This is the default input event associated with the spacebar.

../../_images/start_button_shortcut.png

Nyt kun aloita näppän ilmestyy voit painaa joko sitä tai Space aloittaaksesi pelin.

Projektin tiedostot

Löydät valmiin version tästä projektista näistä osoitteista: