Godot学习(二)—— 使用C#开发第一个2D游戏

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

这个游戏叫做“Dodge the Creeps!”。你的角色必须尽可能长时间移动并避开敌人。gd版本号为3.5.3,代码语言为C#。整体教学参考官方文档,在此基础上进行细节补充。在最后总结部分,列出了每个场景中添加的节点和脚本代码。本文主要供本人学习复盘使用,写的还不是很好,希望大家留下意见,共同进步!

开源项目地址,下载后在该目录的2D文件夹中找到该游戏
Godot3.5.3下载地址

一、设置项目

  1. 打开godot,点击New Project创建项目,需要保存到一个空文件夹中
  2. 将开源项目中art和fonts文件添加到新建项目的文件夹中在这里插入图片描述
  3. 进入编辑器界面,点击Project -> Project Settings,在弹窗的General选项卡下,找到Display -> Window tab,在Size选项中设置游戏场景的宽度Width为480,高度Height为720在这里插入图片描述
  4. 在同窗口下拉找到Stretch选项,将Mode设置为2d,Aspect设置为keep。这样可以确保游戏画面在不同尺寸的屏幕中都能保持比例。在这里插入图片描述
    注:该项目将包含Player、Mob、HUD三个子场景,及一个主场景Main,由于规模较小,场景和脚本文件都一起放在根文件夹res://下。对于大型的项目,应该分别创建各自的文件夹存放在这里插入图片描述

二、创建玩家场景

项目创建完成后,需要在Scene面板添加根节点(把每个Scene场景都看作树状结构,故每个场景都有且只有一个根节点)。如图,点击2D Scene会快速添加Node2D节点,3D Scene会添加Spatial节点,User Interface会添加Control节点,Other Node可以选择所有节点。
在这里插入图片描述

  1. 创建根节点,点击添加Other Node,选择并添加Area2D节点,该节点的作用是检测与玩家重叠或进入玩家内部的物体,双击节点名称并更改为Player在这里插入图片描述
  2. 在添加任何子节点到Player节点前,为确保不因点击子节点而移动或者调整其大小。在这里插入图片描述
  3. 点击导航栏Scene -> Save,将场景命名并保存为Player.tscn。
  4. 点击Player节点并添加子节点AnimatedSprite。该子节点会处理player的外观和动画,然后在右侧Inspector面板找到Frames属性,点击empty -> New SpriteFrame在这里插入图片描述
  5. 点击创建好的SpriteFrame,在屏幕下方显示Animation列表在这里插入图片描述
  6. 将解压后的工程中art文件内对应的资源添加到动画up和walk中在这里插入图片描述
  7. 点击AnimatedSprite节点将Scale属性设置为(0.5,0.5)
  8. 添加子节点CollisionShape2D,将蓝色区域覆盖住整个动画在这里插入图片描述

三、编写玩家代码

  1. 添加Player的移动、动画和检测碰撞脚本代码。注意使用的是后缀为mono版本的gd编辑器,在语言这边选择C#。在这里插入图片描述在这里插入图片描述
    创建完成后的脚本视图,如下。
    创建完成后

  2. 声明Player需要使用的成员变量。其中,在speed上添加export关键字,即可在右侧Inspector中设置其值,在该处修改值会覆盖脚本中的值。
    如果使用的是 C#,则每当要查看新的导出变量或信号时,都需要(重新)构建项目程序集。可以通过点击编辑器右上方的Build或者底部MSBuild中的Build更新变量参数的显示。
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

using Godot;
using System;

public class Player : Area2D
{
    [Export]
    public int Speed = 400; // How fast the player will move (pixels/sec).

    public Vector2 ScreenSize; // Size of the game window.
}
  1. 您的player.gd脚本应该已经包含_ready()_process()函数。当一个节点进入场景树时,会自动调用func _ready() 函数,是一个获得游戏窗口大小的好时机
public override void _Ready()
{
    ScreenSize = GetViewportRect().Size;
}
  1. 现在我们可以使用_process()函数来定义玩家将要做的事情。 _process()在每一帧中都被调用,所以我们将使用它来更新游戏的元素,我们预计这些元素会经常更改。对于玩家,我们需要做到以下几点:
    (1)检查按键输入。
    (2)对象按指定方向移动。
    (3)播放适当的动画来显示移动过程。
  2. 首先,我们需要检查输入,即玩家是否在按键?对于这个游戏,我们有4个方向输入要检查。输入操作在Input Map下的项目设置中定义。在这里,您可以定义自定义事件,并为它们分配不同的键、鼠标事件或其他输入。对于这个游戏,我们将把箭头键映射到四个方向。

点击菜单栏 Project -> Project Settings 打开项目设置窗口,然后单击顶部的Input Map选项卡。在顶部栏中键入“move_right”,然后单击“添加”按钮添加move_right操作。在这里插入图片描述
我们需要为此操作分配一个按键。单击右侧的“+”图标,打开事件管理器窗口。

在这里插入图片描述
“right”键现在与move_right操作相关联。重复这些步骤以添加另外三个映射:
move_left、move_up、move_down。
在这里插入图片描述

  1. 可以使用Input.IsActionPressed()来检测是否按下了某个键,如果按下了,则返回true;如果没有按下,则返回false。
public override void _Process(double delta)
{
    var velocity = Vector2.Zero; // The player's movement vector.

    if (Input.IsActionPressed("move_right"))
    {
        velocity.X += 1;
    }

    if (Input.IsActionPressed("move_left"))
    {
        velocity.X -= 1;
    }

    if (Input.IsActionPressed("move_down"))
    {
        velocity.Y += 1;
    }

    if (Input.IsActionPressed("move_up"))
    {
        velocity.Y -= 1;
    }

    var animatedSprite2D = GetNode<AnimatedSprite2D>("AnimatedSprite2D");

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

其中GetNode<AnimatedSprite2D>("AnimatedSprite2D");的作用是返回指定路径"AnimatedSprite2D"的节点,并最终转化为AnimatedSprite2D类型

Normalized()则是将速度矢量单位化,这样在斜向运动时候的速度不会偏快。

  1. 现在我们有了移动方向,可以更新玩家的位置。我们也可以使用clamp()来防止它离开屏幕。固定值意味着将其限制在给定范围内。将以下内容添加到_process函数的底部(确保它没有缩进到else下):
Position += velocity * delta;
Position = new Vector2(
    x: Mathf.Clamp(Position.x, 0, ScreenSize.x),
    y: Mathf.Clamp(Position.y, 0, ScreenSize.y)
);

_process()函数中的delta参数指的是帧长度,即前一帧完成所需的时间。使用此值可确保即使帧速率发生变化,您的移动也将保持一致。

  1. 在向左和向下运动的时候,分别对动画进行水平和垂直翻转。
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;
}

在确认人物可以正常的移动后,在_ready()函数的末尾添加Hide();在游戏开始的时候隐藏人物。

  1. 检测碰撞。虽然目前还未创建敌人,而我们希望去检测玩家是否被敌人碰撞,可以使用Godot的signal功能。在public class Player : Area2D{ }中添加以下代码:
[Signal]
public delegate void Hit();

这定义了一个称为“Hit”的自定义信号,当玩家与敌人碰撞时,我们会让玩家发出该信号。我们将使用Area2D来检测碰撞。选择Player节点,然后单击检查器选项卡旁边的“Node”选项卡,查看Player可以发出的信号列表:
在这里插入图片描述
注意我们的自定义“Hit”信号也在那里!由于我们的敌人将是RigidBody2D节点,我们想要body_entred(body:Node)信号。当身体接触玩家时,会发出此信号。单击“Connect…”(连接…),此时会出现“Connect a Signal”(连接信号)窗口。我们不需要更改任何这些设置所以请再次单击“连接”。Godot会自动在玩家的脚本中创建一个函数。添加以下代码:

public void OnPlayerBodyEntered(PhysicsBody2D body)
{
    Hide(); // Player disappears after being hit.
    EmitSignal(nameof(Hit));
    // Must be deferred as we can't change physics properties on a physics callback.
    GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}

每当敌人击中玩家时,就会发出信号。我们需要禁用玩家的碰撞,这样我们就不会多次触发命中信号。最后一项是添加一个函数,我们可以在开始新游戏时调用该函数来重置玩家。

public void Start(Vector2 pos)
{
    Position = pos;
    Show();
    GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}

四、创建敌人

  1. 创建Mob场景
    点击Scene-> New Scene 创建场景后添加以下节点:
    在这里插入图片描述
    在RigidBody2D属性中,将“Gravity Scale”设定为0,这样mob就不会向下坠落。此外,在“CollisionObject2D”区域下,单击“Mask”属性并取消选中第一个框。这将确保mob之间不会发生冲突。
    在这里插入图片描述
  2. 设置mob的动画。在AnimatedSprite中创建fly、swim、walk三个动画,并分别将图片资源导入。
    在这里插入图片描述
    然后,将其Playing属性设置为on。Scale设置为0.75、0.75。同时,添加CapsuleShape2D作为碰撞的面积,为了更好地贴合,将Rotation Degrees设置为90。然后保存场景。
  3. 编写mob.cs脚本。新建脚本,在_ready()中,播放动画,并从三种动画类型中随机选择一种:
public class Mob : RigidBody2D
{
    // Don't forget to rebuild the project.
    public override void _Ready()
	{
	    var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
	    animSprite.Playing = true;
	    string[] mobTypes = animSprite.Frames.GetAnimationNames();
	    animSprite.Animation = mobTypes[GD.Randi() % mobTypes.Length];
	}
}

如果希望每次运行场景时“随机”数字序列不同,则必须使用randomize()。我们将在主场景中使用randomize(),所以这里不需要它。
最后一件事是让mob在离开屏幕时删除自己。连接VisibilityNotificationer2D节点的screen_exited()信号,并添加以下代码:

public void OnVisibilityNotifier2DScreenExited()
{
    QueueFree();
}

注意:C#在添加signal的时候,需要确保Receiver Method的名称与在.cs中的函数名称一致,一般不会自动生成、或者会生成.gd中适合的格式然后报错,需要我们手动删除报错的部分,再将函数手动添加到根节点的.cs脚本中
在这里插入图片描述

五、游戏主场景

  1. 新建Main场景,并添加Node作为根节点,其子节点如下:
    在这里插入图片描述
    在这里插入图片描述
  2. 在场景的四周随机产生mob。
    添加Path2D节点,命名为MobPath作为Main的child node。在选中Path2D节点时,会显示下图,选中红色框出的选项,并按gif所示操作。
    在这里插入图片描述
    在这里插入图片描述
    现在路径已经定义,那么添加一个PathFollow2D节点作为MobPath的子节点,并将其命名为MobSpawnLocation。该节点将在路径移动时自动旋转并跟随路径,因此我们可以使用它沿路径选择随机位置和方向。
  3. 添加main的脚本:在脚本的顶部,我们使用export(PackedScene)来选择要实例化的Mob场景。
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 int Score;
    public override void _Ready()
	{
	    GD.Randomize();
	}
}

Build后单击Main节点,您将在Inspector中的“Script Variables”看到Mob Scene属性。点击 "[empty]"旁向下的箭头选中 "Load"并选择 Mob.tscn.
然后,选中Scene dock中的Player节点,并确保在Inspectors的Node中选择了“signal”选项卡。
您应该看到Player节点的信号列表。在列表中找到并双击Hit信号(或右键单击并选择“连接…”)。这将打开信号连接对话框。我们想制作一个名为GameOver的新函数,它将处理游戏结束时需要发生的事情。在信号连接对话框底部的“接收器方法”框中键入GameOver,然后单击“连接”。在新函数中添加以下代码,以及一个NewGame函数,该函数将为新游戏设置一切:

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

现在将每个Timer节点(StartTimer、ScoreTimer和MobTimer)的timeout()信号连接到主脚本。StartTimer将启动另外两个计时器。ScoreTimer将使分数增加1。

public void OnScoreTimerTimeout()
{
    Score++;
}

public void OnStartTimerTimeout()
{
    GetNode<Timer>("MobTimer").Start();
    GetNode<Timer>("ScoreTimer").Start();
}

在OnMobTimerTimeout()中,我们将创建一个mob实例,沿着Path2D选择一个随机起始位置,并设置mob的运动。PathFollow2D节点将在沿路径旋转时自动旋转,因此我们将使用它来选择暴徒的方向及其位置。当我们产生一个暴民时,我们会选择一个150.0到250.0之间的随机值来表示每个暴民的移动速度(如果他们都以相同的速度移动,那会很无聊)。

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 Mob and PathFollow2D, since they appear later on the line.

    // Create a new instance of the Mob scene.
    var mob = (Mob)Mobscene.Instance();

    // Choose a random location on Path2D.
    var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
    mobSpawnLocation.Offset = GD.Randi();

    // 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.
    mob.Position = mobSpawnLocation.Position;

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

    // Choose the velocity.
    var velocity = new Vector2((float)GD.RandRange(150.0, 250.0), 0);
    mob.LinearVelocity = velocity.Rotated(direction);

    // Spawn the mob by adding it to the Main scene.
    AddChild(mob);
}

让我们测试一下场景,确保一切正常。将此NewGame调用添加到_ready():

public override void _Ready()
{
    NewGame();
}

让我们还将Main指定为我们的“主场景”——当游戏启动时自动运行的场景。按下“play”按钮,并在提示时选择Main.tscn。
你应该能够移动玩家,看到mob产生,并看到玩家在被暴徒击中时消失。当您确定一切正常时,请从_ready()中删除对new_game()的调用。

六、游戏信息显示

  1. 创建一个新场景,并添加一个名为HUD的CanvasLayer根节点。HUD是一种信息显示,显示在游戏视图的顶层。
    在这里插入图片描述
  2. 单击ScoreLabel,然后在Inspector的Text字段中键入一个数字。“控制”节点的默认字体较小,缩放效果不佳。游戏资产中包含一个名为“Xolonium Regular.ttf”的字体文件。要使用此字体,请执行以下操作:
    在Theme overrides > font 下,点[empty]并选择“New DynamicFont”
    在这里插入图片描述
    单击您添加的“DynamicFont”,然后在Font>FontData下,选择“load”并选择“Xolonium Regular.ttf”文件。
    在这里插入图片描述
    在“settings”下设置“size”属性,64效果良好。
    在ScoreLabel上完成此操作后,您可以单击Font属性旁边的向下箭头,然后选择“复制”,然后在其他两个Control节点的同一位置“粘贴”它。
    按如下所示排列节点。单击“布局”按钮设置控制节点的布局:

在这里插入图片描述

在这里插入图片描述
在HUD.cs中添加代码:

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信号告诉Main节点按钮已被按下。

public void ShowMessage(string text)
{
    var message = GetNode<Label>("Message");
    message.Text = text;
    message.Show();

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

当我们想要临时显示一条消息时,会调用此函数,例如“Get Ready”。当玩家输了时会调用此函数。它将显示“游戏结束”2秒,然后返回标题屏幕,短暂停顿后显示“开始”按钮。

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();
}
public void UpdateScore(int score)
{
    GetNode<Label>("ScoreLabel").Text = score.ToString();
}
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

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

  1. 将HUD连接到Main。
    现在我们已经完成了HUD场景的创建,返回Main。在Main中实例化HUD场景,就像在Player场景中一样。场景树应该是这样的,所以请确保您没有错过任何内容:

在这里插入图片描述
现在我们需要将HUD功能连接到我们的主脚本。这需要在主场景中添加一些内容:在节点选项卡中,通过在“连接信号”窗口的“接收器方法”中键入“NewGame”,将HUD的Start_game信号连接到主节点的NewGame()函数。
在NewGame()中,更新分数显示并显示“准备就绪”消息:

var hud = GetNode<HUD>("HUD");
hud.UpdateScore(Score);
hud.ShowMessage("Get Ready!");

在GameOver()中,我们需要调用相应的HUD函数:

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

最后,将其添加到OnScoreTimerTimeout()中,以使显示与不断变化的分数保持同步:

GetNode<HUD>("HUD").UpdateScore(Score);

如果你一直玩到“游戏结束”,然后马上开始一个新游戏,上一个游戏的毛骨悚然可能仍然在屏幕上。如果他们在新游戏开始时全部消失,那就更好了。我们只需要一种方法来告诉所有的暴徒离开自己。我们可以通过“群组”功能来实现这一点。
在Mob场景中,选择根节点,然后单击Inspector(与查找节点信号的位置相同)旁边的“节点”选项卡。在“信号”旁边,单击“组”,您可以键入一个新的组名,然后单击“添加”。
现在所有的暴民都将加入“暴民”组。然后,我们可以在Main中的NewGame()函数中添加以下行:

// Note that for calling Godot-provided methods with strings,
// we have to use the original Godot snake_case name.
GetTree().CallGroup("mobs", "queue_free");

CallGroup()函数调用组中每个节点上的命名函数——在这种情况下,我们告诉每个mob删除自己。
游戏在这一点上基本完成了。在下一部分也是最后一部分中,我们将通过添加背景、循环音乐和一些键盘快捷键来对其进行润色。

总结

Player场景中的节点:
Player场景中的节点
最终Player.cs中的代码:

using Godot;
using System;

public class Player : Area2D
{
	// Declare member variables here. Examples:
	// private int a = 2;
	// private string b = "text";
	[Signal]
	public delegate void Hit();

	[Export]
	public int Speed { get; set; } = 400; // How fast the player will move (pixels/sec).

	public Vector2 ScreenSize; // Size of the game window.
	
	// Called when the node enters the scene tree for the first time.
	public override void _Ready()
	{
		ScreenSize = GetViewportRect().Size;
		Hide();
	}

//  // Called every frame. 'delta' is the elapsed time since the previous frame.
	public override void _Process(float delta)
	{
		var velocity = Vector2.Zero; //速度矢量初值设为0向量
		if(Input.IsActionPressed("move_right"))
		{
			velocity.x+=1;		
		}
		if(Input.IsActionPressed("move_left"))
		{
			velocity.x-=1;		
		}
		if(Input.IsActionPressed("move_down"))
		{
			velocity.y+=1;		
		}
		if(Input.IsActionPressed("move_up"))
		{
			velocity.y-=1;		
		}

		var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");

		if(velocity.Length()>0)
		{
			velocity=velocity.Normalized()*Speed;
			animatedSprite.Play();
		}
		else
		{
			animatedSprite.Stop();
		}
		Position+=velocity *delta;
		Position =new Vector2(
			x:Mathf.Clamp(Position.x,0,ScreenSize.x),
			y:Mathf.Clamp(Position.y,0,ScreenSize.y)
		);
		if(velocity.x != 0)
		{
			animatedSprite.Animation="walk";
			animatedSprite.FlipV=false;
			animatedSprite.FlipH=velocity.x <0;
		}
		else if(velocity.y!=0)
		{
			animatedSprite.Animation="up";
			animatedSprite.FlipV=velocity.y>0;
		}
		
	}
	private void OnPlayerBodyEntered(PhysicsBody2D body)
	{
		// Replace with function body.
		Hide(); // Player disappears after being hit.
		EmitSignal(nameof(Hit));
		// Must be deferred as we can't change physics properties on a physics callback.
		GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
	}
	public void Start(Vector2 pos)
	{
		Position = pos;
		Show();
		GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
	}
}

mob场景中的节点:
在这里插入图片描述
mob.cs最终的代码:

using Godot;
using System;

public class Mob : RigidBody2D
{
    // Declare member variables here. Examples:
    // private int a = 2;
    // private string b = "text";

    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
        animSprite.Playing = true;
        string[] mobTypes = animSprite.Frames.GetAnimationNames();
        animSprite.Animation = mobTypes[GD.Randi() % mobTypes.Length];
    }
    public void OnVisibilityNotifier2DScreenExited()
    {
        QueueFree();
    }
    
//  // Called every frame. 'delta' is the elapsed time since the previous frame.
//  public override void _Process(float delta)
//  {
//      
//  }
}

HUD场景中的节点:
在这里插入图片描述
HUD.cs:

using Godot;
using System;

public class HUD : CanvasLayer
{
    // Declare member variables here. Examples:
    // private int a = 2;
    // private string b = "text";
    [Signal]
    public delegate void StartGame();
    public void ShowMessage(string text)
    {
        var message = GetNode<Label>("Message");
        message.Text = text;
        message.Show();

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

    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();
    }
    public void UpdateScore(int score)
    {
        GetNode<Label>("ScoreLabel").Text = score.ToString();
    }
    public void OnStartButtonPressed()
    {
        GetNode<Button>("StartButton").Hide();
        EmitSignal("StartGame");
    }

    public void OnMessageTimerTimeout()
    {
        GetNode<Label>("Message").Hide();
    }
    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        
    }
    
//  // Called every frame. 'delta' is the elapsed time since the previous frame.
//  public override void _Process(float delta)
//  {
//      
//  }
}

Main场景中的节点:
在这里插入图片描述
Main.cs:

using Godot;
using System;

public class Main : Node
{
    // Declare member variables here. Examples:
    // private int a = 2;
    // private string b = "text";
#pragma warning disable 649
    [Export]
    public PackedScene Mobscene;
#pragma warning restore 649
public int Score;
    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        GD.Randomize();
    }

	public void GameOver()
	{	
        GetNode<Timer>("ScoreTimer").Stop();
		GetNode<Timer>("MobTimer").Stop();
        GetNode<HUD>("HUD").ShowGameOver();
	}

	public void NewGame()
	{
		Score = 0;

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

		GetNode<Timer>("StartTimer").Start();
        var hud = GetNode<HUD>("HUD");
        hud.UpdateScore(Score);
        hud.ShowMessage("Get Ready!");
        GetTree().CallGroup("mobs", "queue_free");
	} 
    
    
    public void OnStartTimerTimeout()
    {
        GetNode<Timer>("MobTimer").Start();
        GetNode<Timer>("ScoreTimer").Start();
    }

    public void OnScoreTimerTimeout()
    {
        Score++;
        GetNode<HUD>("HUD").UpdateScore(Score);
    }

  

    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 Mob and PathFollow2D, since they appear later on the line.

        // Create a new instance of the Mob scene.
        var mob = (Mob)Mobscene.Instance();

        // Choose a random location on Path2D.
        var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
        mobSpawnLocation.Offset = GD.Randi();

        // 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.
        mob.Position = mobSpawnLocation.Position;

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

        // Choose the velocity.
        var velocity = new Vector2((float)GD.RandRange(150.0, 250.0), 0);
        mob.LinearVelocity = velocity.Rotated(direction);

        // Spawn the mob by adding it to the Main scene.
        AddChild(mob);
    }
//  // Called every frame. 'delta' is the elapsed time since the previous frame.
    // public override void _Process(float delta)
    // {
        
    // }
}

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值