Godot学习(二)—— 使用C#开发第一个2D游戏
这个游戏叫做“Dodge the Creeps!你的角色必须尽可能长时间移动并避开敌人。gd版本号为3.5.3,代码语言为C#。整体教学参考官方文档,在此基础上进行细节补充。在最后总结部分,列出了每个场景中添加的节点和脚本代码。本文主要供本人学习复盘使用,写的还不是很好,希望大家留下意见,共同进步!开源项目地址,下载后在该目录的2D文件夹中找到该游戏Godot3.5.3下载地址Player场景中的
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
这个游戏叫做“Dodge the Creeps!”。你的角色必须尽可能长时间移动并避开敌人。gd版本号为3.5.3,代码语言为C#。整体教学参考官方文档,在此基础上进行细节补充。在最后总结部分,列出了每个场景中添加的节点和脚本代码。本文主要供本人学习复盘使用,写的还不是很好,希望大家留下意见,共同进步!
开源项目地址,下载后在该目录的2D文件夹中找到该游戏
Godot3.5.3下载地址
一、设置项目
- 打开godot,点击New Project创建项目,需要保存到一个空文件夹中
- 将开源项目中art和fonts文件添加到新建项目的文件夹中
- 进入编辑器界面,点击Project -> Project Settings,在弹窗的General选项卡下,找到Display -> Window tab,在Size选项中设置游戏场景的宽度Width为480,高度Height为720
- 在同窗口下拉找到Stretch选项,将Mode设置为2d,Aspect设置为keep。这样可以确保游戏画面在不同尺寸的屏幕中都能保持比例。
注:该项目将包含Player、Mob、HUD三个子场景,及一个主场景Main,由于规模较小,场景和脚本文件都一起放在根文件夹res://下。对于大型的项目,应该分别创建各自的文件夹存放
二、创建玩家场景
项目创建完成后,需要在Scene面板添加根节点(把每个Scene场景都看作树状结构,故每个场景都有且只有一个根节点)。如图,点击2D Scene会快速添加Node2D节点,3D Scene会添加Spatial节点,User Interface会添加Control节点,Other Node可以选择所有节点。
- 创建根节点,点击添加Other Node,选择并添加Area2D节点,该节点的作用是检测与玩家重叠或进入玩家内部的物体,双击节点名称并更改为Player
- 在添加任何子节点到Player节点前,为确保不因点击子节点而移动或者调整其大小。
- 点击导航栏Scene -> Save,将场景命名并保存为Player.tscn。
- 点击Player节点并添加子节点AnimatedSprite。该子节点会处理player的外观和动画,然后在右侧Inspector面板找到Frames属性,点击empty -> New SpriteFrame
- 点击创建好的SpriteFrame,在屏幕下方显示Animation列表
- 将解压后的工程中art文件内对应的资源添加到动画up和walk中
- 点击AnimatedSprite节点将Scale属性设置为(0.5,0.5)
- 添加子节点CollisionShape2D,将蓝色区域覆盖住整个动画
三、编写玩家代码
-
添加Player的移动、动画和检测碰撞脚本代码。注意使用的是后缀为mono版本的gd编辑器,在语言这边选择C#。
创建完成后的脚本视图,如下。
-
声明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.
}
- 您的player.gd脚本应该已经包含
_ready()
和_process()
函数。当一个节点进入场景树时,会自动调用func _ready()
函数,是一个获得游戏窗口大小的好时机
public override void _Ready()
{
ScreenSize = GetViewportRect().Size;
}
- 现在我们可以使用_process()函数来定义玩家将要做的事情。 _process()在每一帧中都被调用,所以我们将使用它来更新游戏的元素,我们预计这些元素会经常更改。对于玩家,我们需要做到以下几点:
(1)检查按键输入。
(2)对象按指定方向移动。
(3)播放适当的动画来显示移动过程。 - 首先,我们需要检查输入,即玩家是否在按键?对于这个游戏,我们有4个方向输入要检查。输入操作在
Input Map
下的项目设置中定义。在这里,您可以定义自定义事件,并为它们分配不同的键、鼠标事件或其他输入。对于这个游戏,我们将把箭头键映射到四个方向。
点击菜单栏 Project
-> Project Settings
打开项目设置窗口,然后单击顶部的Input Map选项卡。在顶部栏中键入“move_right”,然后单击“添加”按钮添加move_right操作。
我们需要为此操作分配一个按键。单击右侧的“+”图标,打开事件管理器窗口。
“right”键现在与move_right操作相关联。重复这些步骤以添加另外三个映射:
move_left、move_up、move_down。
- 可以使用
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()
则是将速度矢量单位化,这样在斜向运动时候的速度不会偏快。
- 现在我们有了移动方向,可以更新玩家的位置。我们也可以使用
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参数指的是帧长度,即前一帧完成所需的时间。使用此值可确保即使帧速率发生变化,您的移动也将保持一致。
- 在向左和向下运动的时候,分别对动画进行水平和垂直翻转。
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();
在游戏开始的时候隐藏人物。
- 检测碰撞。虽然目前还未创建敌人,而我们希望去检测玩家是否被敌人碰撞,可以使用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;
}
四、创建敌人
- 创建Mob场景
点击Scene
->New Scene
创建场景后添加以下节点:
在RigidBody2D属性中,将“Gravity Scale”
设定为0,这样mob就不会向下坠落。此外,在“CollisionObject2D”区域下,单击“Mask”属性并取消选中第一个框。这将确保mob之间不会发生冲突。
- 设置mob的动画。在
AnimatedSprite
中创建fly、swim、walk三个动画,并分别将图片资源导入。
然后,将其Playing
属性设置为on。Scale
设置为0.75、0.75。同时,添加CapsuleShape2D作为碰撞的面积,为了更好地贴合,将Rotation Degrees
设置为90。然后保存场景。 - 编写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脚本中
五、游戏主场景
- 新建Main场景,并添加
Node
作为根节点,其子节点如下:
- 在场景的四周随机产生mob。
添加Path2D节点,命名为MobPath作为Main的child node。在选中Path2D节点时,会显示下图,选中红色框出的选项,并按gif所示操作。
现在路径已经定义,那么添加一个PathFollow2D节点作为MobPath的子节点,并将其命名为MobSpawnLocation。该节点将在路径移动时自动旋转并跟随路径,因此我们可以使用它沿路径选择随机位置和方向。 - 添加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()的调用。
六、游戏信息显示
- 创建一个新场景,并添加一个名为HUD的CanvasLayer根节点。HUD是一种信息显示,显示在游戏视图的顶层。
- 单击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();
}
- 将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.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)
// {
// }
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)