Fix your first 3D game issues (#6763)
@@ -9,7 +9,7 @@ importing the start assets and setting up the game scene.
|
||||
We've prepared a Godot project with the 3D models and sounds we'll use for this
|
||||
tutorial, linked in the index page. If you haven't done so yet, you can download
|
||||
the archive here: `Squash the Creeps assets
|
||||
<https://github.com/GDQuest/godot-3d-dodge-the-creeps/releases/tag/1.1.0>`__.
|
||||
<https://github.com/godotengine/godot-3d-dodge-the-creeps/releases/tag/1.1.0>`__.
|
||||
|
||||
Once you downloaded it, extract the .zip archive on your computer. Open the
|
||||
Godot project manager and click the *Import* button.
|
||||
@@ -39,7 +39,7 @@ Setting up the playable area
|
||||
----------------------------
|
||||
|
||||
We're going to create our main scene with a plain :ref:`Node <class_Node>` as its root. In the
|
||||
*Scene* dock, click the *Add Node* button represented by a "+" icon in the
|
||||
*Scene* dock, click the *Add Child Node* button represented by a "+" icon in the
|
||||
top-left and double-click on *Node*. Name the node ``Main``. Alternatively, to add
|
||||
a node to the scene, you can press :kbd:`Ctrl + a` (or :kbd:`Cmd + a` on macOS).
|
||||
|
||||
@@ -64,8 +64,7 @@ its shape. If you click the icon, a popup appears to give you more information.
|
||||
.. image:: img/01.game_setup/07.collision_shape_warning.png
|
||||
|
||||
To create a shape, select the :ref:`CollisionShape3D <class_CollisionShape3D>` node, head to the *Inspector*
|
||||
and click the *<empty>* field next to the *Shape* property. Create a new *Box
|
||||
Shape3D*.
|
||||
and click the *<empty>* field next to the *Shape* property. Create a new *BoxShape3D*.
|
||||
|
||||
.. image:: img/01.game_setup/08.create_box_shape3D.jpg
|
||||
|
||||
@@ -86,7 +85,7 @@ with it. Select the ``Ground`` node and add a :ref:`MeshInstance3D <class_MeshIn
|
||||
|
||||
.. image:: img/01.game_setup/10.mesh_instance3d.png
|
||||
|
||||
In the *Inspector*, click on the field next to *Mesh* and create a `BoxMesh <class_BoxMesh>`
|
||||
In the *Inspector*, click on the field next to *Mesh* and create a :ref:`BoxMesh <class_BoxMesh>`
|
||||
resource to create a visible box.
|
||||
|
||||
.. image:: img/01.game_setup/11.box_mesh.webp
|
||||
@@ -103,7 +102,7 @@ We're going to move the ground down so we can see the floor grid. Select the
|
||||
``Ground`` node, hold the :kbd:`Ctrl` key down to turn on grid snapping (:kbd:`Cmd` on macOS),
|
||||
and click and drag down on the Y axis. It's the green arrow in the move gizmo.
|
||||
|
||||
.. image:: img/01.game_setup/13.move_gizmo_y_axis.png
|
||||
.. image:: img/01.game_setup/move_gizmo_y_axis.webp
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -128,11 +127,11 @@ Ultimately, ``Ground``'s transform.position.y should be -1
|
||||
.. image:: img/01.game_setup/ground_down1meter.webp
|
||||
|
||||
Let's add a directional light so our scene isn't all grey. Select the ``Main``
|
||||
node and add a child node :ref:`DirectionalLight <class_DirectionalLight3D>`.
|
||||
node and add a child node :ref:`DirectionalLight3D <class_DirectionalLight3D>`.
|
||||
|
||||
.. image:: img/01.game_setup/create_directional_light3d.webp
|
||||
|
||||
We need to move and rotate the :ref:`DirectionalLight <class_DirectionalLight3D>` node.
|
||||
We need to move and rotate the :ref:`DirectionalLight3D <class_DirectionalLight3D>` node.
|
||||
Move it up by clicking and dragging on the manipulator's green arrow
|
||||
and click and drag on the red arc to rotate it around the X axis, until the
|
||||
ground is lit.
|
||||
|
||||
@@ -23,7 +23,7 @@ character.
|
||||
|
||||
# How fast the player moves in meters per second.
|
||||
@export var speed = 14
|
||||
# The downward acceleration while in the air, in meters per second squared.
|
||||
# The downward acceleration when in the air, in meters per second squared.
|
||||
@export var fall_acceleration = 75
|
||||
|
||||
var target_velocity = Vector3.ZERO
|
||||
@@ -39,7 +39,7 @@ character.
|
||||
// How fast the player moves in meters per second.
|
||||
[Export]
|
||||
public int Speed = 14;
|
||||
// The downward acceleration while in the air, in meters per second squared.
|
||||
// The downward acceleration when in the air, in meters per second squared.
|
||||
[Export]
|
||||
public int FallAcceleration = 75;
|
||||
|
||||
@@ -47,7 +47,7 @@ character.
|
||||
}
|
||||
|
||||
|
||||
These are common properties for a moving body. The ``velocity`` is a :ref:`3D vector <class_Vector3>`
|
||||
These are common properties for a moving body. The ``target_velocity`` is a :ref:`3D vector <class_Vector3>`
|
||||
combining a speed with a direction. Here, we define it as a property because
|
||||
we want to update and reuse its value across frames.
|
||||
|
||||
@@ -186,8 +186,8 @@ fall speed separately. Be sure to go back one tab so the lines are inside the
|
||||
target_velocity.z = direction.z * speed
|
||||
|
||||
# Vertical Velocity
|
||||
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
|
||||
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
|
||||
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
|
||||
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
|
||||
|
||||
# Moving the Character
|
||||
velocity = target_velocity
|
||||
@@ -237,27 +237,27 @@ Here is the complete ``Player.gd`` code for reference.
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
extends CharacterBody3D
|
||||
extends CharacterBody3D
|
||||
|
||||
# How fast the player moves in meters per second.
|
||||
@export var speed = 14
|
||||
# The downward acceleration when in the air, in meters per second squared.
|
||||
@export var fall_acceleration = 75
|
||||
# How fast the player moves in meters per second.
|
||||
@export var speed = 14
|
||||
# The downward acceleration when in the air, in meters per second squared.
|
||||
@export var fall_acceleration = 75
|
||||
|
||||
var target_velocity = Vector3.ZERO
|
||||
var target_velocity = Vector3.ZERO
|
||||
|
||||
|
||||
func _physics_process(delta):
|
||||
var direction = Vector3.ZERO
|
||||
func _physics_process(delta):
|
||||
var direction = Vector3.ZERO
|
||||
|
||||
if Input.is_action_pressed("move_right"):
|
||||
direction.x += 1
|
||||
if Input.is_action_pressed("move_left"):
|
||||
direction.x -= 1
|
||||
if Input.is_action_pressed("move_back"):
|
||||
direction.z += 1
|
||||
if Input.is_action_pressed("move_forward"):
|
||||
direction.z -= 1
|
||||
if Input.is_action_pressed("move_right"):
|
||||
direction.x += 1
|
||||
if Input.is_action_pressed("move_left"):
|
||||
direction.x -= 1
|
||||
if Input.is_action_pressed("move_back"):
|
||||
direction.z += 1
|
||||
if Input.is_action_pressed("move_forward"):
|
||||
direction.z -= 1
|
||||
|
||||
if direction != Vector3.ZERO:
|
||||
direction = direction.normalized()
|
||||
@@ -268,8 +268,8 @@ Here is the complete ``Player.gd`` code for reference.
|
||||
target_velocity.z = direction.z * speed
|
||||
|
||||
# Vertical Velocity
|
||||
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
|
||||
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
|
||||
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
|
||||
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
|
||||
|
||||
# Moving the Character
|
||||
velocity = target_velocity
|
||||
|
||||
@@ -104,15 +104,15 @@ and ``max_speed``, to define a random speed range, which we will later use to de
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
extends CharacterBody3D
|
||||
extends CharacterBody3D
|
||||
|
||||
# Minimum speed of the mob in meters per second.
|
||||
@export var min_speed = 10
|
||||
# Maximum speed of the mob in meters per second.
|
||||
@export var max_speed = 18
|
||||
# Minimum speed of the mob in meters per second.
|
||||
@export var min_speed = 10
|
||||
# Maximum speed of the mob in meters per second.
|
||||
@export var max_speed = 18
|
||||
|
||||
|
||||
func _physics_process(_delta):
|
||||
func _physics_process(_delta):
|
||||
move_and_slide()
|
||||
|
||||
.. code-tab:: csharp
|
||||
@@ -150,7 +150,7 @@ The function will take a ``start_position``,the mob's spawn position, and the
|
||||
|
||||
We position the mob at ``start_position`` and turn it towards the player using
|
||||
the ``look_at_from_position()`` method, and randomize the angle by rotating a
|
||||
random amount around the Y axis. Below, ``rand_range()`` outputs a random value
|
||||
random amount around the Y axis. Below, ``randf_range()`` outputs a random value
|
||||
between ``-PI / 4`` radians and ``PI / 4`` radians.
|
||||
|
||||
.. tabs::
|
||||
@@ -253,7 +253,7 @@ Here is the complete ``Mob.gd`` script for reference.
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
extends CharacterBody3D
|
||||
extends CharacterBody3D
|
||||
|
||||
# Minimum speed of the mob in meters per second.
|
||||
@export var min_speed = 10
|
||||
|
||||
@@ -11,7 +11,7 @@ you will have monsters roaming the game board.
|
||||
Double-click on ``Main.tscn`` in the *FileSystem* dock to open the ``Main`` scene.
|
||||
|
||||
Before drawing the path, we're going to change the game resolution. Our game has
|
||||
a default window size of ``1024x600``. We're going to set it to ``720x540``, a
|
||||
a default window size of ``1152x648``. We're going to set it to ``720x540``, a
|
||||
nice little box.
|
||||
|
||||
Go to *Project -> Project Settings*.
|
||||
|
||||
@@ -234,8 +234,8 @@ With this code, if no collisions occurred on a given frame, the loop won't run.
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
func _physics_process(delta):
|
||||
#...
|
||||
# Iterate through all collisions that occurred this frame
|
||||
#...
|
||||
# Iterate through all collisions that occurred this frame
|
||||
# in C this would be for(int i = 0; i < collisions.Count; i++)
|
||||
for index in range(get_slide_collision_count()):
|
||||
# We get one of the collisions with the player
|
||||
@@ -288,7 +288,7 @@ the :ref:`CharacterBody3D <class_CharacterBody3D>` class and are related to
|
||||
:ref:`KinematicCollision3D<class_KinematicCollision3D>` object that holds
|
||||
information about where and how the collision occurred. For example, we use its
|
||||
``get_collider`` property to check if we collided with a "mob" by calling
|
||||
``is_in_group()`` on it: ``collision.get_collider(index).is_in_group("mob")``.
|
||||
``is_in_group()`` on it: ``collision.get_collider().is_in_group("mob")``.
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ a ``die()`` function that helps us put a descriptive label on the code.
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_mob_detector_body_entered(_body):
|
||||
func _on_mob_detector_body_entered(body):
|
||||
die()
|
||||
|
||||
.. code-tab:: csharp
|
||||
@@ -106,7 +106,7 @@ the character should die when an enemy runs into the collider. Note that without
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
var player_position = $Player.transform.origin
|
||||
var player_position = $Player.position
|
||||
|
||||
gives error because there is no $Player!
|
||||
|
||||
@@ -269,8 +269,8 @@ Next is ``Mob.gd``.
|
||||
queue_free()
|
||||
|
||||
func squash():
|
||||
squashed.emit()
|
||||
queue_free() # Destroy this node
|
||||
squashed.emit()
|
||||
queue_free() # Destroy this node
|
||||
.. code-tab:: csharp
|
||||
|
||||
using Godot;
|
||||
@@ -321,6 +321,7 @@ Finally, the longest script, ``Player.gd``:
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
extends CharacterBody3D
|
||||
|
||||
signal hit
|
||||
@@ -329,6 +330,7 @@ Finally, the longest script, ``Player.gd``:
|
||||
@export var speed = 14
|
||||
# The downward acceleration while in the air, in meters per second squared.
|
||||
@export var fall_acceleration = 75
|
||||
# Vertical impulse applied to the character upon jumping in meters per second.
|
||||
@export var jump_impulse = 20
|
||||
# Vertical impulse applied to the character upon bouncing over a mob
|
||||
# in meters per second.
|
||||
@@ -356,14 +358,14 @@ Finally, the longest script, ``Player.gd``:
|
||||
# Prevent diagonal moving fast af
|
||||
if direction != Vector3.ZERO:
|
||||
direction = direction.normalized()
|
||||
$Pivot.look_at(position + direction,Vector3.UP)
|
||||
$Pivot.look_at(position + direction, Vector3.UP)
|
||||
|
||||
# Ground Velocity
|
||||
target_velocity.x = direction.x * speed
|
||||
target_velocity.z = direction.z * speed
|
||||
|
||||
# Vertical Velocity
|
||||
if not is_on_floor(): # If in the air, fall towards the floor
|
||||
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
|
||||
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
|
||||
|
||||
# Jumping.
|
||||
|
||||
@@ -60,23 +60,14 @@ By default, a theme only has one property, the *Default Font*.
|
||||
interfaces, but that is beyond the scope of this series. To learn more about
|
||||
creating and editing themes, see :ref:`doc_gui_skinning`.
|
||||
|
||||
Click the *Default Font* property and create a new :ref:`FontVariation <class_FontVariation>`
|
||||
|
||||
|image7|
|
||||
|
||||
Expand the :ref:`FontVariation <class_FontVariation>` by clicking on it and expand its *Font* section. There,
|
||||
you will see an empty *Font Data* field.
|
||||
|
||||
|image8|
|
||||
|
||||
This one expects a font file like the ones you have on your computer. Two common
|
||||
font file formats are TrueType Font (TTF) and OpenType Font (OTF).
|
||||
|
||||
In the *FileSystem* dock, expand the ``fonts`` directory and click and drag the
|
||||
``Montserrat-Medium.ttf`` file we included in the project onto the *Font Data*.
|
||||
``Montserrat-Medium.ttf`` file we included in the project onto the *Default Font*.
|
||||
The text will reappear in the theme preview.
|
||||
|
||||
The text is a bit small. Set the *Settings -> Size* to ``22`` pixels to increase
|
||||
The text is a bit small. Set the *Settings -> Default Font Size* to ``22`` pixels to increase
|
||||
the text's size.
|
||||
|
||||
|image9|
|
||||
@@ -125,7 +116,7 @@ line:
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
func _on_mob_timer_timeout():
|
||||
#...
|
||||
#...
|
||||
# We connect the mob to the score label to update the score upon squashing one.
|
||||
mob.squashed.connect($UserInterface/ScoreLabel._on_Mob_squashed.bind())
|
||||
|
||||
@@ -200,7 +191,7 @@ child node *ColorRect*, and name it ``Retry``. This node fills a
|
||||
rectangle with a uniform color and will serve as an overlay to darken the
|
||||
screen.
|
||||
|
||||
To make it span over the whole viewport, you can use the *Layout* menu in the
|
||||
To make it span over the whole viewport, you can use the *Anchor Preset* menu in the
|
||||
toolbar.
|
||||
|
||||
|image12|
|
||||
@@ -267,7 +258,7 @@ Then, when the player gets hit, we show the overlay.
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
func _on_Player_hit():
|
||||
func _on_player_hit():
|
||||
#...
|
||||
$UserInterface/Retry.show()
|
||||
|
||||
@@ -467,15 +458,13 @@ Here is the complete ``Main.gd`` script for reference.
|
||||
.. |image4| image:: img/08.score_and_replay/02.score_label_moved.png
|
||||
.. |image5| image:: img/08.score_and_replay/03.creating_theme.png
|
||||
.. |image6| image:: img/08.score_and_replay/04.theme_preview.png
|
||||
.. |image7| image:: img/08.score_and_replay/05.dynamic_font.webp
|
||||
.. |image8| image:: img/08.score_and_replay/06.font_data.webp
|
||||
.. |image9| image:: img/08.score_and_replay/07.font_size.webp
|
||||
.. |image10| image:: img/08.score_and_replay/08.open_main_script.png
|
||||
.. |image11| image:: img/08.score_and_replay/09.score_in_game.png
|
||||
.. |image12| image:: img/08.score_and_replay/10.layout_icon.png
|
||||
.. |image13| image:: img/08.score_and_replay/11.full_rect_option.png
|
||||
.. |image14| image:: img/08.score_and_replay/12.anchors_updated.png
|
||||
.. |image15| image:: img/08.score_and_replay/13.retry_color_picker.png
|
||||
.. |image15| image:: img/08.score_and_replay/retry_color_picker.webp
|
||||
.. |image16| image:: img/08.score_and_replay/14.retry_node.png
|
||||
.. |image17| image:: img/08.score_and_replay/15.layout_center.png
|
||||
.. |image18| image:: img/08.score_and_replay/16.new_scene.png
|
||||
|
||||
@@ -205,9 +205,9 @@ vector, add the following code.
|
||||
#...
|
||||
if direction != Vector3.ZERO:
|
||||
#...
|
||||
$AnimationPlayer.playback_speed = 4
|
||||
$AnimationPlayer.speed_scale = 4
|
||||
else:
|
||||
$AnimationPlayer.playback_speed = 1
|
||||
$AnimationPlayer.speed_scale = 1
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
@@ -217,11 +217,11 @@ vector, add the following code.
|
||||
if (direction != Vector3.Zero)
|
||||
{
|
||||
// ...
|
||||
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
|
||||
GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
|
||||
GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,14 +271,14 @@ following line.
|
||||
|
||||
func initialize(start_position, player_position):
|
||||
#...
|
||||
$AnimationPlayer.playback_speed = random_speed / min_speed
|
||||
$AnimationPlayer.speed_scale = random_speed / min_speed
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
|
||||
{
|
||||
// ...
|
||||
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = randomSpeed / MinSpeed;
|
||||
GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = randomSpeed / MinSpeed;
|
||||
}
|
||||
|
||||
And with that, you finished coding your first complete 3D game.
|
||||
@@ -293,6 +293,7 @@ Here's the *Player* script.
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
extends CharacterBody3D
|
||||
|
||||
signal hit
|
||||
@@ -568,7 +569,7 @@ And the *Mob*'s script.
|
||||
}
|
||||
|
||||
.. |image0| image:: img/squash-the-creeps-final.gif
|
||||
.. |image1| image:: img/09.adding_animations/01.animation_player_dock.png
|
||||
.. |image1| image:: img/09.adding_animations/animation_player_dock.webp
|
||||
.. |image2| image:: img/09.adding_animations/02.new_animation.webp
|
||||
.. |image3| image:: img/09.adding_animations/03.float_name.png
|
||||
.. |image4| image:: img/09.adding_animations/03.timeline.png
|
||||
|
||||
@@ -17,7 +17,7 @@ Where should you begin? Below, you’ll find a few pages to start exploring and
|
||||
build upon what you’ve learned so far.
|
||||
|
||||
But before that, here’s a link to download a completed version of the project:
|
||||
`<https://github.com/GDQuest/godot-3d-dodge-the-creeps>`_.
|
||||
`<https://github.com/godotengine/godot-3d-dodge-the-creeps>`_.
|
||||
|
||||
Exploring the manual
|
||||
--------------------
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -31,7 +31,7 @@ This tutorial is for beginners who followed the complete getting started series.
|
||||
We'll start slow with detailed instructions and shorten them as we do similar
|
||||
steps. If you're an experienced programmer, you can browse the complete demo's
|
||||
source code here: `Squash the Creep source code
|
||||
<https://github.com/GDQuest/godot-3d-dodge-the-creeps/>`__.
|
||||
<https://github.com/godotengine/godot-3d-dodge-the-creeps>`__.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -42,7 +42,7 @@ source code here: `Squash the Creep source code
|
||||
|
||||
We prepared some game assets so we can jump straight to the code. You can
|
||||
download them here: `Squash the Creeps assets
|
||||
<https://github.com/GDQuest/godot-3d-dodge-the-creeps/releases/tag/1.1.0>`__.
|
||||
<https://github.com/godotengine/godot-3d-dodge-the-creeps/releases/tag/1.1.0>`__.
|
||||
|
||||
We will first work on a basic prototype for the player's movement. We will then
|
||||
add the monsters that we'll spawn randomly around the screen. After that, we'll
|
||||
|
||||