Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Updates 3D Game tutorial to new tutorial structure #463

Merged
merged 1 commit into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export default defineConfig({
},
{
label: '3D Game',
link: 'game-design/godot/3dgame',
link: 'game-design/godot/crystalgame/0-setup/',
},
{
label: 'Setting up C# For Godot',
Expand Down
966 changes: 0 additions & 966 deletions src/content/docs/game-design/godot/3dGame.mdx

This file was deleted.

100 changes: 100 additions & 0 deletions src/content/docs/game-design/godot/crystalgame/0-setup/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
type: tutorial
unitTitle: Setting up the scene
title: Scene setup
---

import Checklist from '/src/components/tutorial/Checklist.astro';
import Box from '/src/components/tutorial/Box.astro';
import { Steps } from '@astrojs/starlight/components';

This is a guide to making a 3-dimensional game in [Godot](https://godotengine.org/). If you are unfamiliar with Godot, check out the [Godot basics](/game-design/godot/basics) doc and the [3D fundmentals](/game-design/godot/3d) doc.

:::note[Version]
This guide is up-to-date with Godot 4.3 stable official release but will most likely work with any 4.x release.
:::

We'll be making a game in which the player defends an objective against constantly spawning waves of enemies, with both a lose and win condition. Designed to act as a foundation for your own ideas, allowing for easy expansion and polish.

## Making the project
Set up a Basic 3D project, using the Forward+ Renderer. Create a Node3D as the root, and call it 'World'

![Godot new project window](/src/assets/godot/3DGameGuide/3dgameprojectsetup.png)

## Working with the 3D Viewport

When working with 2D Space, we work on two axes, X and Y. When working in a 3D space, we add a third, Z. In Godot, Y represents up and down, while X and Y represent the two horizontal axes.

While mousing over the 3D game viewport, holding **right-click** will allow you to fly around the viewport using **WASD**. You can use the **scroll wheel** to control your speed. While not holding **right-click** you can use the **scroll wheel** to zoom in and out.

Now that you know the basics, let's get into making our game!

## Creating a first-person controller

:::note[Godot Documentation]
Godot Documentation for nodes discussed in this section:

[StaticBody3D](https://docs.godotengine.org/en/stable/classes/class_staticbody3d.html) [MeshInstance3D](https://docs.godotengine.org/en/stable/classes/class_meshinstance3d.html) [CollisionShape3D](https://docs.godotengine.org/en/stable/classes/class_collisionshape3d.html)
[CharacterBody3D](https://docs.godotengine.org/en/stable/classes/class_characterbody3d.html)
:::

Let's set up a basic character controller. Thankfully, Godot makes this easy for us and actually has a template script that'll let us move around.

## Scene Setup

First, let's give ourselves something to stand on.
<Steps>
1. Create a **Node3D** and name it 'World' or 'Level' this'll be the root of our whole scene.
2. Create a **StaticBody3D** and give it two child nodes, a **MeshInstance3D** and a **CollisionShape3D.** The mesh provides visuals for our floor, while the collisionShape is what we actually stand on.
3. Select the **MeshInstance3D** and over on the right, in the inspector, assign its Mesh property to be a **PlaneMesh**
4. Select the **CollisionShape3D** and set its 'Shape' property to a new **BoxShape3D**
5. Select **CollisionShape3D** in the scene tree and use the orange dots in the viewport to shape the **BoxShape3D** to the same shape and size as the plane. Although it's good to leave it a little thicker than the plane, to stop us from falling through.
6. Click on the **StaticBody3D** and find the **Transform** over on the right. Increase any of the Scale values to something like 20. They'll all increase as they're 'linked' (Denoted by the chain on the right)
</Steps>

Great! Now our player has something to stand on. Let's add our Character.

## Adding the Character
<Steps>
1. Create a **CharacterBody3D** as a child of our root world node. right-click on it, and save it as a new scene. This allows us to easily modify our player. Name it 'Player' or 'Character'
2. Find the newly created scene in the file explorer, or click on the "scene" icon, to open our scene.
2. Give it a **Camera3D** and a **CollisionShape3D** as children. Given the collisionShape a capsule shape using the inspector. The CollisionShape is what'll allow us to collide with the floor.
3. right-click on the **CharacterBody3D** and assign a script, leave everything as default and hit load. This is because we're using the script Godot provides for us. For now we won't be messing with this, you can click 3D at the top to return to the scene view.
5. Let's click on the **Camera3D** over on the left, and use the green arrow in the viewport to move it on the Y-axis to wherever you think the 'eyes' of your character should be, based on the capsule.
6. Make sure you save the scene, using **CTRL + S** or using the File menu in the top left.
7. Let's go back to our main scene now, using the tab with the name you used for your 'World' Scene. You'll probably notice that the player is halfway in the floor, which is not ideal. Just click on the root **CharacterBody3D** Node and move to up on the Y-Axis
8. As a final touch, let's add a **DirectionalLight3D** and rotate it to face downwards so that we can see!
</Steps>

Great! Let's test our game! Hit the **Run Project** Button and try moving around!
You'll probably notice two things:

1. We can't look around
2. The default controls use the arrow keys to move, when WASD is generally standard.

Don't worry, we'll fix these issues shortly.


:::note[StaticBody vs RigidBody?]
You may have noticed we used a **StaticBody3D** Object. If you've ever used a 3D game engine before, you've likely heard of 'Static Bodies' and 'Rigid Bodies' but what's the difference?

Both exist as part of the games Physics simulation, and are capable of physically affecting other objects. The main difference is how they're affected by other objects.

A Static Body cannot be moved by any other physics object, hence 'static' (Though scripts can still move them) Think a solid wall, or tree. Most objects in a given scene that want a player to collide with, will be Static Bodies.

A Rigid Body is the opposite, and can bounce and fall and move based on collisions. If one rigidbody hits another, it'll make it move, based on things like velocity and gravity. Think a soccer ball colliding with another.
:::
### Some things to try

1. Try deleting the collision shape from the ground or the player, what happens?

2. Open up the Script for the character movement, try changing the speed up or down, what happens?

<Box>
## Checklist
<Checklist>
- [ ] I have something to stand on
- [ ] I can move around using the arrow keys
- [ ] I'm ready to make a game!
</Checklist>
</Box>
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
---
type: tutorial
unitTitle: Character controller improvement
title: Improving our Character
---

import Checklist from '/src/components/tutorial/Checklist.astro';
import Box from '/src/components/tutorial/Box.astro';
import { Steps } from '@astrojs/starlight/components';

:::note[Godot Documentation]
Godot Documentation for nodes discussed in this section:

[Input Map](https://docs.godotengine.org/en/stable/classes/class_inputmap.html) [Input Examples](https://docs.godotengine.org/en/stable/tutorials/inputs/input_examples.html)
[Camera3D](https://docs.godotengine.org/en/stable/classes/class_camera3d.html) [Exports](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_exports.html)
:::
### Changing the Controls

First, let's fix our control scheme. If you want, you can leave your controls as the arrowkeys, or make them whatever you like. But it's a good idea to understand how to change them.
Thankfully, Godot has a robust Input System.
<Steps>

1. Let's open our input settings by going to **Project > Project Settings... > Input Map** from the top left menu. Using the 'Add new Action' field, add five actions. 'Forward', 'Backward', 'Left', 'Right', and 'Jump'

2. Using the '+' button next to each direction, simple press the key you want to assign for each. Let's do 'W', 'S', 'A', 'D', and 'Space' respectively. Makes sure you're not holding shift or control, otherwise these will become part of the input.

</Steps>

Great! But if you hit play now, you'll notice nothing has changed. This is because we need to tell our controller script to listen for these inputs.

Let's go back to our character controller script and change out the default inputs, to our new ones.

:::note[Case sensitivity]
The names of these inputs are case sensitive, so make sure they match exactly what you called them!
:::

<Steps>

1. We'll change

```gdscript
var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
```

to

```gdscript
var input_dir = Input.get_vector("Left", "Right", "Forward", "Backward")
```
2. and we'll change

```gdscript
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
```

to

```gdscript
if Input.is_action_just_pressed("Jump") and is_on_floor():
```
3. Great! Run your game and test these changes, make sure every movement input, and jump works, and double-check any that don't.
</Steps>

In the future, if you want to change the controls, all you'll have to do is change the assigned inputs under the Input Map.

### Fixing the Camera

The final thing we need to do to have a fully functional Character controller, is to get our camera moving!

If we were working on a large long-term project we would likely want to make this its own script, but let's keep things simple for now and just add it to our character controller script.

<Steps>
1. Let's open our character controller script by clicking on the CharacterBody3D node and opening the script tab.

2. Below the existing variable declarations, let's add a new line.

```gdscript
@export var camera:Camera3D
```
The export tag allows us to assign variables/nodes from within the inspector. If you've used unity, this works the same as [SerializeField] We'll take another look at this soon.

3. We'll need two variables to keep track of our rotation, and control our mouse sensitivity, add these underneath the other variable declarations.

```gdscript
var camera_rotation = Vector2(0, 0)
var mouse_sensitivity := 0.005
```
4. You've probably noticed by now, that when launching the game, the mouse stays visible, when most games 'capture' the mouse. Thankfully this is an easy addition using godots Input system.

```gdscript
func _ready() -> void:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
```

The **_ready()** function is called by godot when the object is first instantiated, so its a great place to do things like this.

5. Trying running the game now, you'll notice your mouse is gone! To quit you can alt+tab out, and press stop in the top right menu.

But let's make sure we can get our mouse back.

```gdscript
func _input(event) -> void:
# If escape is pressed reveal the mouse
if event.is_action_pressed("ui_cancel"):
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
```
The **_input(event)** function is called by godot whenever input is detected, this let's us check what was pressed. In this case we'll be checking for 'ui_cancel'. You may notice that we didn't create any input called 'ui_cancel'
This is another one of Godot's default inputs, and is bound to 'esc'

Great! Let's try running our game again, using Esc to get our mouse back.

6. Because this function checks for input, we can use it to tell if our mouse has moved, as Godot considers this input.

Let's add some more to the **_input(event)** function.

```gdscript
if event is InputEventMouseMotion:
# Get how much the mouse has moved and pass it on to the camera_look function
var relative_position = event.relative * mouse_sensitivity
camera_look(relative_position)
```

7. meaning the full function would look like this:

```gdscript
func _input(event) -> void:
# If escape is pressed reveal the mouse
if event.is_action_pressed("ui_cancel"):
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

# Get the mouse movement
if event is InputEventMouseMotion:
# Get how much the mouse has moved and pass it on to the camera_look function
var relative_position = event.relative * mouse_sensitivity
camera_look(relative_position)
```

The **event.relative** variable tracks the difference in the position of the mouse from the last frame, which tells us how much the mouse has moved. We then multiply this by our sensitivity value.

8. You'll notice we've called a method called **camera_look()** that doesn't exist, we'll create this now.

```gdscript
func camera_look(movement: Vector2) -> void:
# Add how much the camera has moved to the camera rotation
camera_rotation += movement
# Stop the player from making the camera go upside down by looking too far up and down
camera_rotation.y = clamp(camera_rotation.y, deg_to_rad(-90), deg_to_rad(90))

# Reset the transform basis
transform.basis = Basis()
main_camera.transform.basis = Basis()

#The player and camera needs to rotate on the x and only the camera should rotate on the y
rotate_object_local(Vector3.UP, -camera_rotation.x)
main_camera.rotate_object_local(Vector3.RIGHT, -camera_rotation.y)
```

This looks complicated, but really what's happening is:
1. We keep track of the total amount the player is rotated by
2. We ensure that the Y rotation can't go over a certain value, to prevent us doing flips with the camera (In this case the value has to stay between -90 and 90 degrees)
3. We zero the basis' of the camera and player, to making adding rotation easy
4. We rotate the Player on the X-axis, and the Camera on the Y-axis. These are separated. as if we simply rotated our player, as we looked up, our player would lean backwards until it falls over.

9. Finally, we need to revisit that ***@export*** statement. make sure you save your script, then go back to our Player scene. Click on the CharacterBody3D and you'll notice that over on the right in the inspector, is a slot called 'Camera'
Just drag and drop our Character's camera from the left panel, over into the inspector, and our camera is assigned!

</Steps>
When we play our game, we can properly walk and look around! Now it's time to get some gameplay in our game!

#### Some things to try

1. Try increasing and decreasing the Camera sensitivity, change it to a value that feels best for you.
2. See if you can change the button we use to get our mouse back to something else

<Box>
## Checklist
<Checklist>
- [ ] I've changed my inputs
- [ ] My camera is controllable with the mouse
</Checklist>
</Box>
Loading
Loading