第3部分

Part overview

In this part, we will be limiting the player’s weapons by giving them ammo. We will also be giving the player the ability to reload, and we will be adding sounds when the weapons fire.

../../../_images/PartThreeFinished.png

注解

在继续本教程的这一部分之前,我们假设您已经完成了 第2部分。 完成的项目来自 :ref:`doc_fps_tutorial_part_two`将成为第3部分的起始项目

让我们开始吧!

改变水平

Now that we have a fully working FPS, let’s move to a more FPS-like level.

打开 Space_Level.tscn (assets / Space_Level_Objects / Space_Level.tscn)和/或 Ruins_Level.tscn (assets / Ruin_Level_Objects / Ruins_Level.tscn)。

Space_Level.tscn and Ruins_Level.tscn are complete custom FPS levels created for the purpose of this tutorial. Press Play Current Scene button, or F6 on keyboard, and give each a try.

警告

Space_Level.tscn``对于GPU的图形要求比``Ruins_Level.tscn``更高。 如果您的计算机正在努力渲染 ``Space_Level.tscn ,请尝试使用 Ruins_Level.tscn 代替。

注解

Due to Godot updates since this tutorial was published, if you are using Godot 3.2 or later, you may need to apply the following changes to the Space Level and Ruins Level scenes:

  • Open res://assets/Space_Level_Objects/Space_Level.tscn.
  • In the Scene tree dock, select the Floor_and_Celing node. In the Inspector dock, if the Mesh Library field under GridMap is [empty], set it to Space_Level_Mesh_Lib.tres by dragging the file res://assets/Space_Level_Objects/Space_Level_Mesh_Lib.tres from the FileSystem dock to that field.
  • Do the same for the Walls node.
  • Open res://assets/Ruin_Level_Objects/Ruins_Level.tscn.
  • In the Scene tree dock, select the Floor node. In the Inspector dock, if the Mesh Library field under GridMap is [empty], set it to Ruin_Level_Mesh_Lib.tres by dragging the file res://assets/Ruin_Level_Objects/Ruin_Level_Mesh_Lib.tres from the FileSystem dock into that field.
  • Do the same for the Walls node.

您可能已经注意到有几个 RigidBody 节点放在整个关卡中。 我们可以在它们上面放置 RigidBody_hit_test.gd 然后它们会对被子弹击中做出反应,所以让我们这样做吧!

按照以下说明选择您要使用的场景中的任何一个(或两个)

Space_Level.tscn

Ruins_Level.tscn

  1. Expand "Other_Objects" and then expand "Physics_Objects".
  2. Expand one of the "Barrel_Group" nodes and then select "Barrel_Rigid_Body" and open it using
  3. the "Open in Editor" button.
  4. This will bring you to the "Barrel_Rigid_Body" scene. From there, select the root node and
  5. scroll the inspector down to the bottom.
  6. Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
  7. "RigidBody_hit_test.gd" and select "Open".
  8. Return back to "Space_Level.tscn".
  9. Expand one of the "Box_Group" nodes and then select "Crate_Rigid_Body" and open it using the
  10. "Open in Editor" button.
  11. This will bring you to the "Crate_Rigid_Body" scene. From there, select the root node and
  12. scroll the inspector down to the bottom.
  13. Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
  14. "RigidBody_hit_test.gd" and select "Open".
  15. Return to "Space_Level.tscn".
  1. Expand "Misc_Objects" and then expand "Physics_Objects".
  2. Select all the "Stone_Cube" RigidBodies and then in the inspector scroll down to the bottom.
  3. Select the drop down arrow under the "Node" tab, and then select "Load". Navigate to
  4. "RigidBody_hit_test.gd" and select "Open".
  5. Return to "Ruins_Level.tscn".

Now you can fire at all the rigid bodies in either level and they will react to bullets hitting them!

添加弹药

现在游戏角色有枪,让我们给他们一些有限的弹药。

首先,我们需要在每个武器脚本中定义一些变量。

打开 Weapon_Pistol.gd 并添加以下类变量:

  1. var ammo_in_weapon = 10
  2. var spare_ammo = 20
  3. const AMMO_IN_MAG = 10
  • ammo_in_weapon:手枪中弹药的数量
  • spare_ammo:我们为手枪留下的弹药量
  • AMMO_IN_MAG:武器或弹仓完全重装后所容纳的弹药数量

现在我们需要做的就是在 fire_weapon 中添加一行代码。

在``Clone.BULLET_DAMAGE = DAMAGE``下添加以下内容:ammo_in_weapon - = 1

This will remove one from ammo_in_weapon every time the player fires. Notice we’re not checking to see if the player has enough ammo or not in fire_weapon. Instead, we’re going to check to see if the player has enough ammo in Player.gd.


现在我们需要为步枪和刀子添加弹药。

注解

You may be wondering why we are adding ammo for the knife given it does not consume any ammunition. The reason we want to add ammo to the knife is so we have a consistent interface for all our weapons.

If we did not add ammo variables for the knife, we would have to add checks for the knife. By adding the ammo variables to the knife, we don’t need to worry about whether or not all our weapons have the same variables.

将以下类变量添加到``Weapon_Rifle.gd``:

  1. var ammo_in_weapon = 50
  2. var spare_ammo = 100
  3. const AMMO_IN_MAG = 50

然后将以下内容添加到``fire_weapon``:ammo_in_weapon - = 1。 确保``ammo_in_weapon - = 1``在``if ray.is_colliding()``之后检查,这样无论游戏角色是否击中某个东西,游戏角色都会失去弹药。

现在剩下的就是刀。 将以下内容添加到``Weapon_Knife.gd``:

  1. var ammo_in_weapon = 1
  2. var spare_ammo = 1
  3. const AMMO_IN_MAG = 1

Because the knife does not consume ammo, that is all we need to add.


现在我们需要在“ Player.gd”中更改一件事,也就是说,

how we’re firing the weapons in process_input. Change the code for firing weapons to the following:

  1. # ----------------------------------
  2. # Firing the weapons
  3. if Input.is_action_pressed("fire"):
  4. if changing_weapon == false:
  5. var current_weapon = weapons[current_weapon_name]
  6. if current_weapon != null:
  7. if current_weapon.ammo_in_weapon > 0:
  8. if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
  9. animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
  10. # ----------------------------------

现在武器的弹药数量有限,并且当游戏角色用尽时将停止射击。


Ideally, we’d like to let the player be able to see how much ammo is left. Let’s make a new function called process_UI.

首先,将 process_UI(delta) 添加到 _physics_process

现在将以下内容添加到 Player.gd:

  1. func process_UI(delta):
  2. if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
  3. UI_status_label.text = "HEALTH: " + str(health)
  4. else:
  5. var current_weapon = weapons[current_weapon_name]
  6. UI_status_label.text = "HEALTH: " + str(health) + \
  7. "\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo)

让我们回顾一下发生的事情:

Firstly, we check to see if the current weapon is either UNARMED or KNIFE. If it is, we change the UI_status_label‘s text to only show the player’s health since UNARMED and KNIFE do not consume ammo.

如果游戏角色正在使用消耗弹药的武器,我们首先获得武器节点。

Then we change UI_status_label‘s text to show the player’s health, along with how much ammo the player has in the weapon and how much spare ammo the player has for that weapon.

现在我们可以看到游戏角色通过HUD获得了多少弹药。

添加重装到武器

现在游戏角色可以用尽弹药,我们需要一种方法让游戏角色填补它们。 我们接下来再添加重装!

For reloading, we need to add a few more variables and a function to every weapon.

打开 Weapon_Pistol.gd 并添加以下类变量:

  1. const CAN_RELOAD = true
  2. const CAN_REFILL = true
  3. const RELOADING_ANIM_NAME = "Pistol_reload"
  • CAN_RELOAD:一个布尔值,用于跟踪此武器是否具有重新加载的能力
  • CAN_REFILL:一个布尔值,用于跟踪我们是否可以重新填充此武器的备用弹药。 我们不会在这部分使用 CAN_REFILL ,但我们将在下一部分中使用!
  • RELOADING_ANIM_NAME:此武器的重新加载动画的名称。

现在我们需要添加一个处理重载的函数。 将以下函数添加到``Weapon_Pistol.gd``:

  1. func reload_weapon():
  2. var can_reload = false
  3. if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
  4. can_reload = true
  5. if spare_ammo <= 0 or ammo_in_weapon == AMMO_IN_MAG:
  6. can_reload = false
  7. if can_reload == true:
  8. var ammo_needed = AMMO_IN_MAG - ammo_in_weapon
  9. if spare_ammo >= ammo_needed:
  10. spare_ammo -= ammo_needed
  11. ammo_in_weapon = AMMO_IN_MAG
  12. else:
  13. ammo_in_weapon += spare_ammo
  14. spare_ammo = 0
  15. player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)
  16. return true
  17. return false

让我们回顾一下发生的事情:

首先,我们定义一个变量,以查看此武器是否可以重新加载。

然后我们检查游戏角色是否处于这个武器的空闲动画状态,因为我们只希望能够在游戏角色没有开火,装备或无装备时重新加载。

Next we check to see if the player has spare ammo, and if the ammo already in the weapon is equal to a fully reloaded weapon. This way we can ensure the player cannot reload when the player has no ammo or when the weapon is already full of ammo.

If we can still reload, then we calculate the amount of ammo needed to reload the weapon.

如果游戏角色有足够的弹药来填充武器,我们从 spare_ammo 中移除所需的弹药,然后将 ammo_in_weapon 设置为武器/弹仓的满载值。

If the player does not have enough ammo, we add all the ammo left in spare_ammo, and then set spare_ammo to 0.

接下来我们播放这个武器的重新加载动画,然后返回 true

如果游戏角色无法重新加载,我们会返回“false”。


现在我们需要为步枪添加重装。 打开 Weapon_Rifle.gd 并添加以下类变量:

  1. const CAN_RELOAD = true
  2. const CAN_REFILL = true
  3. const RELOADING_ANIM_NAME = "Rifle_reload"

这些变量与手枪完全相同,只是将“RELOADING_ANIM_NAME”改为步枪的重装动画。

现在我们需要将 reload_weapon 添加到``Weapon_Rifle.gd``:

  1. func reload_weapon():
  2. var can_reload = false
  3. if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
  4. can_reload = true
  5. if spare_ammo <= 0 or ammo_in_weapon == AMMO_IN_MAG:
  6. can_reload = false
  7. if can_reload == true:
  8. var ammo_needed = AMMO_IN_MAG - ammo_in_weapon
  9. if spare_ammo >= ammo_needed:
  10. spare_ammo -= ammo_needed
  11. ammo_in_weapon = AMMO_IN_MAG
  12. else:
  13. ammo_in_weapon += spare_ammo
  14. spare_ammo = 0
  15. player_node.animation_manager.set_animation(RELOADING_ANIM_NAME)
  16. return true
  17. return false

This code is exactly the same as the one for the pistol.


我们需要为武器做的最后一点是向刀子添加“重装”。 将以下类变量添加到``Weapon_Knife.gd``:

  1. const CAN_RELOAD = false
  2. const CAN_REFILL = false
  3. const RELOADING_ANIM_NAME = ""

由于我们都无法重新加载或重新填充刀,我们将两个常量都设置为“false”。 我们还将 RELOADING_ANIM_NAME 定义为空字符串,因为该刀没有重新加载动画。

现在我们需要添加``reloading_weapon``:

  1. func reload_weapon():
  2. return false

由于我们无法重装刀,我们总是返回 false

添加重新加载到游戏角色

现在我们需要在 Player.gd 中添加一些内容。 首先,我们需要定义一个新的类变量:

  1. var reloading_weapon = false
  • reloading_weapon:一个变量,用于跟踪游戏角色当前是否正在尝试重新加载。

接下来我们需要为 _physics_process 添加另一个函数调用。

process_reloading(delta) 添加到 _physics_process 。 现在 _physics_process 应该是这样的:

  1. func _physics_process(delta):
  2. process_input(delta)
  3. process_movement(delta)
  4. process_changing_weapons(delta)
  5. process_reloading(delta)
  6. process_UI(delta)

现在我们需要添加 process_reloading 。 将以下函数添加到``Player.gd``:

  1. func process_reloading(delta):
  2. if reloading_weapon == true:
  3. var current_weapon = weapons[current_weapon_name]
  4. if current_weapon != null:
  5. current_weapon.reload_weapon()
  6. reloading_weapon = false

让我们回顾一下这里发生的事情。

Firstly, we check to make sure the player is trying to reload.

如果游戏角色正在尝试重新加载,我们将获得当前的武器。 如果当前武器不等于 null ,我们称之为 reload_weapon 函数。

注解

如果当前武器等于“null”,则当前武器为“UNARMED”。

Finally, we set reloading_weapon to false because, regardless of whether the player successfully reloaded, we’ve tried reloading and no longer need to keep trying.


在我们让游戏角色重新加载之前,我们需要在 process_input 中更改一些内容。

The first thing we need to change is in the code for changing weapons. We need to add an additional check (if reloading_weapon == false:) to see if the player is reloading:

  1. if changing_weapon == false:
  2. # New line of code here!
  3. if reloading_weapon == false:
  4. if WEAPON_NUMBER_TO_NAME[weapon_change_number] != current_weapon_name:
  5. changing_weapon_name = WEAPON_NUMBER_TO_NAME[weapon_change_number]
  6. changing_weapon = true

这使得如果游戏角色重新加载,游戏角色无法改变武器。

现在我们需要添加代码以在游戏角色按下“reload”动作时触发重新加载。 将以下代码添加到``process_input``:

  1. # ----------------------------------
  2. # Reloading
  3. if reloading_weapon == false:
  4. if changing_weapon == false:
  5. if Input.is_action_just_pressed("reload"):
  6. var current_weapon = weapons[current_weapon_name]
  7. if current_weapon != null:
  8. if current_weapon.CAN_RELOAD == true:
  9. var current_anim_state = animation_manager.current_state
  10. var is_reloading = false
  11. for weapon in weapons:
  12. var weapon_node = weapons[weapon]
  13. if weapon_node != null:
  14. if current_anim_state == weapon_node.RELOADING_ANIM_NAME:
  15. is_reloading = true
  16. if is_reloading == false:
  17. reloading_weapon = true
  18. # ----------------------------------

让我们回顾一下这里发生的事情。

首先,我们确保游戏角色没有重新加载,游戏角色也不会尝试更换武器。

然后我们检查是否按下了 reload 动作。

If the player has pressed reload, we then get the current weapon and check to make sure it is not null. Then we check to see whether the weapon can reload or not using its CAN_RELOAD constant.

如果武器可以重新加载,我们将获得当前动画状态,并创建一个变量来跟踪游戏角色是否已经重新加载。

然后我们通过每一件武器来确保游戏角色还没有玩过那个武器的重装动画。

If the player is not reloading any weapon, we set reloading_weapon to true.


我想补充的一件事是,如果您试图发射武器并且没有弹药,那么武器会自动重装。

We also need to add an additional if check (is_reloading_weapon == false:) so the player cannot fire the current weapon while reloading.

让我们在 process_input 中更改我们的触发代码,以便在尝试触发空武器时重新加载:

  1. # ----------------------------------
  2. # Firing the weapons
  3. if Input.is_action_pressed("fire"):
  4. if reloading_weapon == false:
  5. if changing_weapon == false:
  6. var current_weapon = weapons[current_weapon_name]
  7. if current_weapon != null:
  8. if current_weapon.ammo_in_weapon > 0:
  9. if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
  10. animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
  11. else:
  12. reloading_weapon = true
  13. # ----------------------------------

Now we check to make sure the player is not reloading before we fire the weapon, and when we have 0 or less ammo in the current weapon, we set reloading_weapon to true if the player tries to fire.

This will make it so the player will try to reload when attempting to fire an empty weapon.


With that done, the player can now reload! Give it a try! Now you can fire all the spare ammo for each weapon.

添加声音

Finally, let’s add some sounds that accompany the player firing, reloading and changing weapons.

小技巧

本教程中没有提供游戏声音(出于法律原因)。 https://gamesounds.xyz/是 ``免版税或公共领域音乐和适合游戏的声音``的集合 。 我使用了Gamemaster的Gun Sound Pack,可以在Sonniss.com GDC 2017 Game Audio Bundle中找到。

Open up Simple_Audio_Player.tscn. It is simply a Spatial with an AudioStreamPlayer as its child.

注解

这被称为“简单”音频播放器的原因是因为我们没有考虑性能,因为代码旨在以最简单的方式提供声音。

如果您想使用3D音频,所以它听起来像是来自3D空间中的一个位置,右键单击 AudioStreamPlayer 并选择“更改类型”。

这将打开节点浏览器。 导航到 AudioStreamPlayer3D 并选择“更改”。 在本教程的源代码中,我们将使用 AudioStreamPlayer,但如果需要,您可以选择使用 AudioStreamPlayer3D,无论哪一个,下面提供的代码都可以使用 您选择了。

创建一个新脚本并将其命名为 Simple_Audio_Player.gd 。 将它附加到 Simple_Audio_Player.tscn 中的 Spatial 并插入以下代码:

  1. extends Spatial
  2. # All of the audio files.
  3. # You will need to provide your own sound files.
  4. var audio_pistol_shot = preload("res://path_to_your_audio_here")
  5. var audio_gun_cock = preload("res://path_to_your_audio_here")
  6. var audio_rifle_shot = preload("res://path_to_your_audio_here")
  7. var audio_node = null
  8. func _ready():
  9. audio_node = $Audio_Stream_Player
  10. audio_node.connect("finished", self, "destroy_self")
  11. audio_node.stop()
  12. func play_sound(sound_name, position=null):
  13. if audio_pistol_shot == null or audio_rifle_shot == null or audio_gun_cock == null:
  14. print ("Audio not set!")
  15. queue_free()
  16. return
  17. if sound_name == "Pistol_shot":
  18. audio_node.stream = audio_pistol_shot
  19. elif sound_name == "Rifle_shot":
  20. audio_node.stream = audio_rifle_shot
  21. elif sound_name == "Gun_cock":
  22. audio_node.stream = audio_gun_cock
  23. else:
  24. print ("UNKNOWN STREAM")
  25. queue_free()
  26. return
  27. # If you are using an AudioStreamPlayer3D, then uncomment these lines to set the position.
  28. #if audio_node is AudioStreamPlayer3D:
  29. # if position != null:
  30. # audio_node.global_transform.origin = position
  31. audio_node.play()
  32. func destroy_self():
  33. audio_node.stop()
  34. queue_free()

小技巧

By setting position to null by default in play_sound, we are making it an optional argument, meaning position doesn’t necessarily have to be passed in to call play_sound.

让我们回顾一下这里发生的事情:


In _ready, we get the AudioStreamPlayer and connect its finished signal to the destroy_self function. It doesn’t matter if it’s an AudioStreamPlayer or AudioStreamPlayer3D node, as they both have the finished signal. To make sure it is not playing any sounds, we call stop on the AudioStreamPlayer.

警告

Make sure your sound files are not set to loop! If it is set to loop, the sounds will continue to play infinitely and the script will not work!

``play_sound``函数是我们将从``Player.gd``调用的函数。 我们检查声音是否是三种可能的声音之一,如果它是三种声音之一,我们将音频流设置为 AudioStreamPlayer 到正确的声音。

如果是未知声音,我们会向控制台输出错误消息并释放音频播放器。

If you are using an AudioStreamPlayer3D, remove the # to set the position of the audio player node so it plays at the correct position.

最后,我们告诉 AudioStreamPlayer 来玩。

AudioStreamPlayer 播放声音时,它将调用 destroy_self ,因为我们连接了 _ready 中的 finished 信号。 我们停止 AudioStreamPlayer 并释放音频播放器以节省资源。

注解

这个系统非常简单,有一些重大缺陷:

One flaw is we have to pass in a string value to play a sound. While it is relatively simple to remember the names of the three sounds, it can be increasingly complex when you have more sounds. Ideally, we’d place these sounds in some sort of container with exposed variables so we do not have to remember the name(s) of each sound effect we want to play.

另一个缺陷是我们无法使用此系统轻松播放循环音效,也无法播放背景音乐。 因为我们无法播放循环声音,某些效果(如脚步声)难以实现,因为我们必须跟踪是否存在声音效果以及是否需要继续播放它。

这个系统最大的缺点之一是我们只能播放“Player.gd”中的声音。 理想情况下,我们希望能够随时播放任何脚本中的声音。


With that done, let’s open up Player.gd again. First we need to load the Simple_Audio_Player.tscn. Place the following code in the class variables section of the script:

  1. var simple_audio_player = preload("res://Simple_Audio_Player.tscn")

现在我们需要在需要时实例化简单的音频播放器,然后调用它的 play_sound 函数并传递我们想要播放的声音的名称。 为了简化这个过程,让我们在 Player.gd 中创建一个 create_sound 函数:

  1. func create_sound(sound_name, position=null):
  2. var audio_clone = simple_audio_player.instance()
  3. var scene_root = get_tree().root.get_children()[0]
  4. scene_root.add_child(audio_clone)
  5. audio_clone.play_sound(sound_name, position)

Let’s walk through what this function does:


The first line instances the Simple_Audio_Player.tscn scene and assigns it to a variable named audio_clone.

第二行获取场景根,这有一个很大(虽然安全)的假设。

我们首先得到这个节点 SceneTree,然后访问根节点,在这种情况下是 Viewport 这整个游戏正在运行。 然后我们得到了第一个子节点 Viewport,在我们的示例中恰好是 Test_Area.tscn 中的根节点或任何其他提供的级别。 我们正在做出一个巨大的假设,即根节点的第一个子节点是游戏角色所处的根场景,这可能并非总是如此

If this doesn’t make sense to you, don’t worry too much about it. The second line of code only does not work reliably if you have multiple scenes loaded as children of the root node at a time, which will rarely happen for most projects and will not be happening in this tutorial series. This is only potentially a issue depending on how you handle scene loading.

第三行将我们新创建的 Simple_Audio_Player 场景添加为场景根的子节点。 这与我们产生子弹时的工作方式完全相同。

最后,我们调用 play_sound 函数并将传入的参数传递给 create_sound 。 这将使用传入的参数调用 Simple_Audio_Player.gdplay_sound 函数。


现在剩下的就是在我们想要的时候播放声音。 让我们首先为手枪添加声音!

打开 Weapon_Pistol.gd

现在,我们想在游戏角色发射手枪时发出噪音,所以将以下内容添加到 fire_weapon 函数的末尾:

  1. player_node.create_sound("Pistol_shot", self.global_transform.origin)

现在,当游戏角色发射手枪时,我们将发出“手枪射击”的声音。

要在游戏角色重新加载时发出声音,我们需要在 reload_weapon 函数中的 player_node.animation_manager.set_animation(RELOADING_ANIM_NAME) 下添加以下内容:

  1. player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)

现在当游戏角色重新加载时,我们将播放 Gun_cock 声音。


现在让我们为步枪添加声音。 打开 Weapon_Rifle.gd

要在步枪被射击时发出声音,请将以下内容添加到 fire_weapon 函数的末尾:

  1. player_node.create_sound("Rifle_shot", ray.global_transform.origin)

现在,当游戏角色发射步枪时,我们将发出“Rifle_shot”声音。

要在游戏角色重新加载时发出声音,我们需要在 reload_weapon 函数中的 player_node.animation_manager.set_animation(RELOADING_ANIM_NAME) 下添加以下内容:

  1. player_node.create_sound("Gun_cock", player_node.camera.global_transform.origin)

现在当游戏角色重新加载时,我们将播放 Gun_cock 声音。

最后的笔记

../../../_images/PartThreeFinished.png

现在您拥有有限弹药的武器,当您开火时它们会播放声音!

At this point, we have all the basics of an FPS game working. There are still a few things that would be nice to add, and we’re going to add them in the next three parts!

例如,现在我们无法为我们的备件添加弹药,所以我们最终会耗尽。 另外,我们没有任何东西可以射击 RigidBody 节点。

在:参考:`doc_fps_tutorial_part_four`我们将添加一些射击目标,以及一些健康和弹药拾取! 我们还将添加joypad支持,因此我们可以使用有线Xbox 360控制器!

警告

如果您迷路了,请务必再次阅读代码!

您可以在这里下载这个部分的完成项目: Godot_FPS_Part_3.zip