diff --git a/astro.config.mjs b/astro.config.mjs
index 2d2b7a39..f8e1cde7 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -74,12 +74,6 @@ export default defineConfig({
label: "Godot", items: [{
label: "Godot Basics",
link: "game-design/godot/basics"
- },{
- label: "2D Platformer",
- link: "game-design/godot/2dplatformergame"
- },{
- label: "2D Racing Game",
- link: "game-design/godot/2dracinggame"
},{
label: "Universal Features",
link: "game-design/godot/universal"
@@ -98,7 +92,10 @@ export default defineConfig({
},{
label: "Setting up C# For Godot",
link: "game-design/godot/projectsetup"
- }],
+ },{
+ label: "3D Racing Game",
+ link: "game-design/godot/3dracinggame/0-main-scene/"
+ }],
collapsed: true
}],
},
diff --git a/src/assets/godot/2DPlatformer/screenshot1.png b/src/assets/godot/2DPlatformer/screenshot1.png
deleted file mode 100644
index 3a6b2d65..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot1.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot10.png b/src/assets/godot/2DPlatformer/screenshot10.png
deleted file mode 100644
index 9da104ad..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot10.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot11.png b/src/assets/godot/2DPlatformer/screenshot11.png
deleted file mode 100644
index fa3afcc6..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot11.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot12.png b/src/assets/godot/2DPlatformer/screenshot12.png
deleted file mode 100644
index c505d133..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot12.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot2.png b/src/assets/godot/2DPlatformer/screenshot2.png
deleted file mode 100644
index f67b881e..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot2.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot3.png b/src/assets/godot/2DPlatformer/screenshot3.png
deleted file mode 100644
index f6a68dd5..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot3.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot4.png b/src/assets/godot/2DPlatformer/screenshot4.png
deleted file mode 100644
index 29a66c95..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot4.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot5.png b/src/assets/godot/2DPlatformer/screenshot5.png
deleted file mode 100644
index 8fb03f9e..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot5.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot6.png b/src/assets/godot/2DPlatformer/screenshot6.png
deleted file mode 100644
index 33e0e454..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot6.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot7.png b/src/assets/godot/2DPlatformer/screenshot7.png
deleted file mode 100644
index b27b6d93..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot7.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot8.png b/src/assets/godot/2DPlatformer/screenshot8.png
deleted file mode 100644
index 86f83d83..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot8.png and /dev/null differ
diff --git a/src/assets/godot/2DPlatformer/screenshot9.png b/src/assets/godot/2DPlatformer/screenshot9.png
deleted file mode 100644
index 4feaab11..00000000
Binary files a/src/assets/godot/2DPlatformer/screenshot9.png and /dev/null differ
diff --git a/src/assets/godot/2DRacing/image1.png b/src/assets/godot/2DRacing/image1.png
deleted file mode 100644
index 12b2a76f..00000000
Binary files a/src/assets/godot/2DRacing/image1.png and /dev/null differ
diff --git a/src/assets/godot/2DRacing/image2.png b/src/assets/godot/2DRacing/image2.png
deleted file mode 100644
index c0c586da..00000000
Binary files a/src/assets/godot/2DRacing/image2.png and /dev/null differ
diff --git a/src/assets/godot/2DRacing/image3.png b/src/assets/godot/2DRacing/image3.png
deleted file mode 100644
index 4061c4ae..00000000
Binary files a/src/assets/godot/2DRacing/image3.png and /dev/null differ
diff --git a/src/assets/godot/2DRacing/image4.png b/src/assets/godot/2DRacing/image4.png
deleted file mode 100644
index 1b7acf3f..00000000
Binary files a/src/assets/godot/2DRacing/image4.png and /dev/null differ
diff --git a/src/assets/godot/2DRacing/image5.png b/src/assets/godot/2DRacing/image5.png
deleted file mode 100644
index 9479f466..00000000
Binary files a/src/assets/godot/2DRacing/image5.png and /dev/null differ
diff --git a/src/assets/godot/2DRacing/image6.png b/src/assets/godot/2DRacing/image6.png
deleted file mode 100644
index a24c77b3..00000000
Binary files a/src/assets/godot/2DRacing/image6.png and /dev/null differ
diff --git a/src/assets/godot/3DRacing/HUDFileStructure.png b/src/assets/godot/3DRacing/HUDFileStructure.png
new file mode 100644
index 00000000..457fc6a3
Binary files /dev/null and b/src/assets/godot/3DRacing/HUDFileStructure.png differ
diff --git a/src/assets/godot/3DRacing/Path3DControls.png b/src/assets/godot/3DRacing/Path3DControls.png
new file mode 100644
index 00000000..b8878d41
Binary files /dev/null and b/src/assets/godot/3DRacing/Path3DControls.png differ
diff --git a/src/assets/godot/3DRacing/VehicleBodySimple.png b/src/assets/godot/3DRacing/VehicleBodySimple.png
new file mode 100644
index 00000000..a6df7568
Binary files /dev/null and b/src/assets/godot/3DRacing/VehicleBodySimple.png differ
diff --git a/src/assets/godot/3DRacing/VehicleWheelsSimple.png b/src/assets/godot/3DRacing/VehicleWheelsSimple.png
new file mode 100644
index 00000000..d3cc4a22
Binary files /dev/null and b/src/assets/godot/3DRacing/VehicleWheelsSimple.png differ
diff --git a/src/assets/godot/3DRacing/addingPizzazzToCar.png b/src/assets/godot/3DRacing/addingPizzazzToCar.png
new file mode 100644
index 00000000..4d99881d
Binary files /dev/null and b/src/assets/godot/3DRacing/addingPizzazzToCar.png differ
diff --git a/src/assets/godot/3DRacing/area3dFinishLine.png b/src/assets/godot/3DRacing/area3dFinishLine.png
new file mode 100644
index 00000000..16a033d2
Binary files /dev/null and b/src/assets/godot/3DRacing/area3dFinishLine.png differ
diff --git a/src/assets/godot/3DRacing/barrierPositionNumbers.png b/src/assets/godot/3DRacing/barrierPositionNumbers.png
new file mode 100644
index 00000000..57e925bd
Binary files /dev/null and b/src/assets/godot/3DRacing/barrierPositionNumbers.png differ
diff --git a/src/assets/godot/3DRacing/boostGameRunning.png b/src/assets/godot/3DRacing/boostGameRunning.png
new file mode 100644
index 00000000..c1028441
Binary files /dev/null and b/src/assets/godot/3DRacing/boostGameRunning.png differ
diff --git a/src/assets/godot/3DRacing/boostReloadingGameRunning.png b/src/assets/godot/3DRacing/boostReloadingGameRunning.png
new file mode 100644
index 00000000..1afd4454
Binary files /dev/null and b/src/assets/godot/3DRacing/boostReloadingGameRunning.png differ
diff --git a/src/assets/godot/3DRacing/builtCarFileStructure.png b/src/assets/godot/3DRacing/builtCarFileStructure.png
new file mode 100644
index 00000000..f352a068
Binary files /dev/null and b/src/assets/godot/3DRacing/builtCarFileStructure.png differ
diff --git a/src/assets/godot/3DRacing/curvedPath3D.png b/src/assets/godot/3DRacing/curvedPath3D.png
new file mode 100644
index 00000000..bcc73abc
Binary files /dev/null and b/src/assets/godot/3DRacing/curvedPath3D.png differ
diff --git a/src/assets/godot/3DRacing/editingPathPointHeights.png b/src/assets/godot/3DRacing/editingPathPointHeights.png
new file mode 100644
index 00000000..1b56c735
Binary files /dev/null and b/src/assets/godot/3DRacing/editingPathPointHeights.png differ
diff --git a/src/assets/godot/3DRacing/endResultPreview.png b/src/assets/godot/3DRacing/endResultPreview.png
new file mode 100644
index 00000000..ba13bf9e
Binary files /dev/null and b/src/assets/godot/3DRacing/endResultPreview.png differ
diff --git a/src/assets/godot/3DRacing/finishLine.png b/src/assets/godot/3DRacing/finishLine.png
new file mode 100644
index 00000000..ad3f8347
Binary files /dev/null and b/src/assets/godot/3DRacing/finishLine.png differ
diff --git a/src/assets/godot/3DRacing/groundMeshProperties.png b/src/assets/godot/3DRacing/groundMeshProperties.png
new file mode 100644
index 00000000..158ff086
Binary files /dev/null and b/src/assets/godot/3DRacing/groundMeshProperties.png differ
diff --git a/src/assets/godot/3DRacing/levelsMenuScreen.png b/src/assets/godot/3DRacing/levelsMenuScreen.png
new file mode 100644
index 00000000..fc8b2af9
Binary files /dev/null and b/src/assets/godot/3DRacing/levelsMenuScreen.png differ
diff --git a/src/assets/godot/3DRacing/newTrack.png b/src/assets/godot/3DRacing/newTrack.png
new file mode 100644
index 00000000..23b016e7
Binary files /dev/null and b/src/assets/godot/3DRacing/newTrack.png differ
diff --git a/src/assets/godot/3DRacing/pathCurrentLookWithRoad.png b/src/assets/godot/3DRacing/pathCurrentLookWithRoad.png
new file mode 100644
index 00000000..87897483
Binary files /dev/null and b/src/assets/godot/3DRacing/pathCurrentLookWithRoad.png differ
diff --git a/src/assets/godot/3DRacing/pathMeasurements.png b/src/assets/godot/3DRacing/pathMeasurements.png
new file mode 100644
index 00000000..bfa4e330
Binary files /dev/null and b/src/assets/godot/3DRacing/pathMeasurements.png differ
diff --git a/src/assets/godot/3DRacing/pauseMenuHUD.png b/src/assets/godot/3DRacing/pauseMenuHUD.png
new file mode 100644
index 00000000..507d174d
Binary files /dev/null and b/src/assets/godot/3DRacing/pauseMenuHUD.png differ
diff --git a/src/assets/godot/3DRacing/repeatingAPicForGround.png b/src/assets/godot/3DRacing/repeatingAPicForGround.png
new file mode 100644
index 00000000..bb773a69
Binary files /dev/null and b/src/assets/godot/3DRacing/repeatingAPicForGround.png differ
diff --git a/src/assets/godot/3DRacing/tiltButton.png b/src/assets/godot/3DRacing/tiltButton.png
new file mode 100644
index 00000000..7207655b
Binary files /dev/null and b/src/assets/godot/3DRacing/tiltButton.png differ
diff --git a/src/assets/godot/3DRacing/titleScreen.png b/src/assets/godot/3DRacing/titleScreen.png
new file mode 100644
index 00000000..40bc703c
Binary files /dev/null and b/src/assets/godot/3DRacing/titleScreen.png differ
diff --git a/src/assets/godot/3DRacing/track1fileStructure.png b/src/assets/godot/3DRacing/track1fileStructure.png
new file mode 100644
index 00000000..0a2f90b7
Binary files /dev/null and b/src/assets/godot/3DRacing/track1fileStructure.png differ
diff --git a/src/assets/godot/3DRacing/trackBarrierPositions.png b/src/assets/godot/3DRacing/trackBarrierPositions.png
new file mode 100644
index 00000000..8744893d
Binary files /dev/null and b/src/assets/godot/3DRacing/trackBarrierPositions.png differ
diff --git a/src/assets/godot/3DRacing/trackPathResult.png b/src/assets/godot/3DRacing/trackPathResult.png
new file mode 100644
index 00000000..89851e61
Binary files /dev/null and b/src/assets/godot/3DRacing/trackPathResult.png differ
diff --git a/src/assets/godot/3DRacing/worldCurrentFileStructure.png b/src/assets/godot/3DRacing/worldCurrentFileStructure.png
new file mode 100644
index 00000000..f52565c5
Binary files /dev/null and b/src/assets/godot/3DRacing/worldCurrentFileStructure.png differ
diff --git a/src/components/starlight/MobileTableOfContents.astro b/src/components/starlight/MobileTableOfContents.astro
index 5d2cf663..8d91bf48 100644
--- a/src/components/starlight/MobileTableOfContents.astro
+++ b/src/components/starlight/MobileTableOfContents.astro
@@ -22,7 +22,7 @@ const { toc } = Astro.props;
{isTutorial ? (
-
+
) : (
)}
diff --git a/src/components/tutorial/TutorialNav.astro b/src/components/tutorial/TutorialNav.astro
index b066a6f3..ead43165 100644
--- a/src/components/tutorial/TutorialNav.astro
+++ b/src/components/tutorial/TutorialNav.astro
@@ -10,7 +10,7 @@ import Progress from './Progress.astro';
import UnitProgressIcon from './UnitProgressIcon.astro';
const currentUrl = Astro.url.pathname.replace(/\/$/, '');
-const { id } = Astro.props;
+const { id, mobile } = Astro.props;
export const allPages = await getCollection('docs');
const pages = allPages.filter((page) => isTutorialEntry(page, id));
@@ -18,7 +18,7 @@ const pages = allPages.filter((page) => isTutorialEntry(page, id));
const tutorialPages = getTutorialPages(pages);
const units = getTutorialUnits(tutorialPages);
-const makeUnitId = (index: number) => `${Astro.props.id}__tutorial-unit-nav-panel-${index}`;
+const makeUnitId = (index: number) => `${mobile ? "mobile" : ""}__tutorial-unit-nav-panel-${index}`;
const isCurrentUnit = (unit: (typeof units)[number]) =>
unit.lessons.some((lesson) => currentUrl.endsWith(lesson.slug));
---
diff --git a/src/content/docs/game-design/godot/2dPlatformerGame.mdx b/src/content/docs/game-design/godot/2dPlatformerGame.mdx
deleted file mode 100644
index 089ea7aa..00000000
--- a/src/content/docs/game-design/godot/2dPlatformerGame.mdx
+++ /dev/null
@@ -1,370 +0,0 @@
----
-title: 2D Platformer
-description: This page is a step by step guide to making a 2D platformer
-sidebar:
- order: 2
----
-
-This is a tutorial for a basic 2D platformer in Godot. We will be starting with a new project.
-
-Please download the asset pack linked [here](https://brackeysgames.itch.io/brackeys-platformer-bundle) (it is free so you don't need to pay).
-
-## Making the Player
-
-import { Steps } from '@astrojs/starlight/components';
-
-
-
-1. Click "Other Node" and add a CharacterBody2D. Rename this to Player.
-
-2. In the Inspector, click on “Collision” in the CollisionObject2D section and change the layer to only have 2 selected
-
-3. Give your CharacterBody2D 2 child nodes, an AnimatedSprite2D and a CollisionShape2D.
-
-4. In the project menu at the top left, open the project settings, go to the rendering heading in the general tab, click on texture, and change the default texture filter to Nearest. ![Screenshot showing the settings menu](/src/assets/godot/2DPlatformer/screenshot1.png)
-
-
-
-
-
-
-1. Click on AnimatedSprite2D and click on “Animation” in the inspector.
-
-2. Click on Sprite Frames and New SpriteFrames.
-
-3. Click on the SpriteFrames you just made. This should open a SpriteFrames dock at the bottom of the screen. Here we are going to add the sprite animations for our character.
-
-4. Click on the new animation button at the top left of the new dock and create 4 new animations so you have 5 total. These will be the Idle, Run, Roll, Hit, and Death animations. ![Screenshot showing new animation button](/src/assets/godot/2DPlatformer/screenshot2.png)
-
-
-
-
-### Animating The Sprite
-
-
-
-1. Starting with Idle, click the grid icon to the right of the play/pause buttons. Navigate to the knight.png image in the sprites folder of the asset pack and open it. ![Screenshot showing add sprite frames button](/src/assets/godot/2DPlatformer/screenshot3.png)
-
-2. On the right, you will need to change both Vertical and Horizontal to 8, as the images are in an 8x8 grid.
-
-3. Select the first 4 squares as these are our Idle animation frames. Add these frames and you can now watch your knight idle if you click the play button.
-
-4. Make sure to have this animation auto play by clicking the icon to the right of the trash can above the “Filter Animations” bar. ![Screenshot showing auto play on load button](/src/assets/godot/2DPlatformer/screenshot4.png)
-
-5. Now repeat these steps (not including the auto play) for each of the animations.
-
-6. Click on CollisionShape2D in the Node Tree and make the Shape in the Inspector a New CapsuleShape2D. Modify it so that it roughly covers the knight. ![Screenshot showing CollisionShape covering player](/src/assets/godot/2DPlatformer/screenshot5.png)
- :::tip
- Making this shape smaller makes the game easier for the player. At this stage of the design, making the shape slightly smaller than the sprite is recommended. You can experiment with this to find a size you like for your design.
- :::
-
-
-
-You can save this scene as we are done with the knight for right now.
-
-## Creating a Level
-
-
-
-1. Create a new scene by clicking the + button in the scene tabs. Add a 2D Scene and rename it Level 1.
-
-2. Click and drag your Player scene from the FileSystem into the Level 1 Scene Node Tree as a child of Level 1.
-
-3. Add a Camera2D node as a child of the Player and set the Zoom for it in the inspector to 4 for both x and y. If you now run the game, you should see your knight floating in a gray void.
-
-4. Add a TileMapLayer node as a child of Level 1. Rename this to Foreground and create a new Tile Set using the Inspector. This should open 2 new menus at the bottom of the screen, a TileSet menu and a TileMap menu. ![Screenshot showing TileSet and Tilemap menus](/src/assets/godot/2DPlatformer/screenshot6.png)
-
-5. Inside the TileSet menu, click the + button and select “Atlas”.
-
-6. Navigate to the world_tileset.png image in the sprites folder and load that. You will be asked if Godot can automatically create tiles, select yes.
-
-7. Click the “TileSet” button in the inspector. Go to the “Physics Layers” drop down and click the “Add Element” button. ![Screenshot showing physics layer add element](/src/assets/godot/2DPlatformer/screenshot7.png)
-
-8. Move back to the TileSet menu. Click the Paint button and then click the drop down that appears and select “Physics Layer 0”.
-
-9. Click on some of the square platforms and they should be given a blue collision box. It will look highlighted on the Base Tiles sections. ![Screenshot showing TileMap collision boxes](/src/assets/godot/2DPlatformer/screenshot8.png)
- :::tip
- When you click on a sprite, you should see it appear in the Paint window. You can then drag the white diamonds so that the blue collision box roughly covers the sprite. For things like the ground sprites, the default square is ok, but you may need to play around for other sprites, such as the bridge sprites.
- :::
-
-10. Now open the TileMap menu at the bottom of the screen and click on a tile to enable drawing in the editor. You can click to place individual tiles, or click and drag to paint them following the cursor. Play around with the tile paint tools to create a fun level to play. Some cool things to check out are the Rect tool and the Place Random Tile options.
-
-11. Add a new TileMapLayer as a child of Level 1 and place it above Foreground in the Scene Tree. ![Screenshot showing scene tree](/src/assets/godot/2DPlatformer/screenshot9.png)
-
-12. Repeat the steps used for the Foreground (ignoring the physics steps) to create your Background.
- :::tip
- If you placed the Background TileMapLayer above the Foreground, Background tiles should appear behind the Foreground tiles. Unless your player is below the TileMapLayers, they will disappear behind the background, so make sure to move them below the Foreground in the Scene Tree.
- :::
-
-
-
-
-
-## Adding Movement
-
-Now we are going to add gravity and movement to the player.
-
-
-
-1. Go back to your Player scene and attach a script by right clicking the player node and clicking “Attach Script”. Click “Create” as the defaults are what we want. Now if you run your game, you should be able to run around.
- :::tip
- You may want to reduce your speed and jump height, depending on how you want the game to feel (I recommend a speed of 150 and a jump velocity of -300).
- :::
-
-
-2. You may notice that your player doesn't flip when going to the left, nor does the animation change from idle to run when you're moving. To solve this, add the below code
- ```gdscript
- if velocity.x > 0:
- sprite_node.flip_h = false
- elif velocity.x < 0:
- sprite_node.flip_h = true
-
- if abs(velocity.x) > 0:
- sprite_node.animation = "Run"
- else:
- sprite_node.animation = "Idle"
- ```
- above `move_and_slide()` as well as adding `@onready var sprite_node = $AnimatedSprite2D` below `extends CharacterBody2D` as shown below. Note that the animation names are case sensitive, so if your player disappears that may be why.
-
- ```gdscript
- extends CharacterBody2D
-
- @onready var sprite_node = $Sprite
-
- const SPEED = 150.0
- const JUMP_VELOCITY = -300.0
-
-
- func _physics_process(delta: float) -> void:
- # Add the gravity.
- if not is_on_floor():
- velocity += get_gravity() * delta
-
- # Handle jump.
- if Input.is_action_just_pressed("ui_select") and is_on_floor():
- velocity.y = JUMP_VELOCITY
-
- # Get the input direction and handle the movement/deceleration.
- # As good practice, you should replace UI actions with custom gameplay actions.
- var direction := Input.get_axis("ui_left", "ui_right")
- if direction:
- velocity.x = direction * SPEED
- else:
- velocity.x = move_toward(velocity.x, 0, SPEED)
-
- if velocity.x > 0:
- sprite_node.flip_h = false
- elif velocity.x < 0:
- sprite_node.flip_h = true
-
- if abs(velocity.x) > 0:
- sprite_node.animation = "Run"
- else:
- sprite_node.animation = "Idle"
-
- move_and_slide()
- ```
-
-
-
-## Adding Killzones
-
-Next let's make a killzone so that when you fall off the map or hit an enemy, you'll restart the level.
-
-
-
-1. Create a new scene with an Area2D node and rename it Killzone. Go to Collision in the Inspector and change the mask to only have 2 selected.
-
-2. Add a Timer node as a child of Killzone and change the wait time in the Inspector to 0 (this will auto change to 0.001).
-
-3. Attach a script to Killzone. Default options are ok.
-
-4. Click on Timer and in the node menu on the right connect the “timeout” signal to the Killzone script. Now click on Killzone and in the node menu, connect the “body entered” signal to the Killzone script. Replace the all of the code in the script with this:
- ```gdscript
- extends Area2D
-
- @onready var timer = $Timer
-
- func _on_body_entered(body:Node2D):
- timer.start()
-
-
- func _on_timer_timeout():
- get_tree().reload_current_scene()
- ```
-
-5. After saving, you can now drag this scene into your Level 1 scene.
-
-6. Give the Killzone a CollisionShape2D as a child node and make the shape a New WorldBoundaryShape2D. Drag this below your platforms so the player can fall into it. ![Screenshot showing WorldBoundary](/src/assets/godot/2DPlatformer/screenshot10.png)
- :::tip
- You don't need to stretch it out as it will automatically cover the bottom of the scene.
- :::
-
-
-
-
-
-## Making Enemies
-
-
-
-1. Create a new scene with a CharacterBody2D and rename this to Slime.
-
-2. Give it 3 child nodes: an AnimatedSprite2D, a CollisionShape2D, and a Killzone. The Killzone will also need a CollisionShape2D as a child.
-
-3. To add the sprite, simply repeat the steps we used for the player, but this time picking a different sprite (e.g. slime_green.png).
-
-4. For the Killzone, add a New RectangleShape2D to the CollisionShape2D and make it roughly cover the slime. Make the other CollisionShape2D the same as the Killzone's one.
-
-5. We need to give it some way to recognise walls and turn around. Add a RayCast2D as a child of Slime and move it to the centre of the sprite and rotate the arrow so it points forward and stays close to the edge of the sprite.
-
-6. Duplicate this node and flip the arrow the other way. At this point I'd suggest renaming them RayCastLeft and RayCastRight (or something similar) so it's not confusing which is which when we put them in code. ![Screenshot showing slime scene](/src/assets/godot/2DPlatformer/screenshot11.png)
-
-7. Now attach a script to Slime and change all the code to this:
- ```gdscript
- extends CharacterBody2D
-
-
- const SPEED = 30.0
- var direction = 1
- @onready var sprite_node = $AnimatedSprite2D
- @onready var raycast_left = $RayCastLeft
- @onready var raycast_right = $RayCastRight
-
- func _physics_process(delta: float) -> void:
- # Add the gravity.
- if not is_on_floor():
- velocity += get_gravity() * delta
-
-
- if raycast_left.is_colliding():
- direction = 1
- elif raycast_right.is_colliding():
- direction = -1
-
- if velocity.x > 0:
- sprite_node.flip_h = false
- elif velocity.x < 0:
- sprite_node.flip_h = true
-
- velocity.x = direction * SPEED
-
- move_and_slide()
- ```
-
-
-
-Now you can add your Slime to your level and if you run into it, you will reset the level.
-
-
-## Adding Items
-
-Now let's add a coin pickup.
-
-
-
-1. Create a new scene with an Area2D node and rename it Coin.
-
-2. Change the collision mask to only have 2 selected.
-
-3. Give this node a AnimatedSprite2D and a CollisionShape2D as children. Add the coin sprite and animation to the node as before.
-
-4. Give the CollisionShape2D a New CircleShape2D and have it cover the coin.
-
-5. Attach a script to the Area2D node and connect the “body entered” signal to it. Inside the `_on_body_entered` function make it print something so we can test that it's working. If you put the coin in your level, it should print out your message when you run over it. To make the coin disappear when you run over it, add `queue_free()` after the print.
-
-
-
-
-
-## Connecting Levels
-
-Now let's create a trigger to load the next level.
-
-
-
-1. In Level 1 add a new Area2D node and give it a CollisionShape2D as a child.
-
-2. On the Area2D node, change the collision mask to only have 2 selected.
-
-3. Give the CollisionShape2D a shape and place it where you'd like the end of the level to be.
-
-4. Attach a script to the Level 1 node, then click on the new Area2D node you just made and connect the “body entered” signal to Level 1. Change the code to this:
- ```gdscript
- extends Node2D
-
- signal level1_finished
-
-
- func _on_area_2d_body_entered(body:Node2D) -> void:
- level1_finished.emit()
- ```
-
-5. Create a new Scene with a Node2D called Game and attach a script to it. Change the code to below and make sure the preload filepath goes to your Level 1 scene.
- ```gdscript
- extends Node2D
-
- var level1 = preload("res://Scenes/level1.tscn")
-
- func _ready() -> void:
- var l = level1.instantiate()
- l.connect("level1_finished", _on_level1_finished)
- add_child(l)
-
- func _on_level1_finished():
- for n in get_children():
- remove_child(n)
- n.queue_free()
- ```
-
-6. Go into Project -> Project Settings -> Application -> Run and change the main scene to your new Game scene.
- ![Screenshot showing changing the main scene](/src/assets/godot/2DPlatformer/screenshot12.png) If you now run the project, you should be met with a gray void upon reaching the end of the level.
-
-7. You can make another scene called Level 2 and once you've done that, change the code in the Game script to the below:
- ```gdscript
- extends Node2D
-
- var level1 = preload("res://Scenes/level1.tscn")
- var level2 = preload("res://Scenes/level2.tscn")
- var levels = [level1, level2]
- var current_level = 0
-
- func _ready() -> void:
- var l = levels[current_level].instantiate()
- l.connect("level1_finished", _on_level1_finished)
- call_deferred("add_child", l)
-
- func reload_current_level():
- for n in get_children():
- remove_child(n)
- n.queue_free()
-
- var l = levels[current_level].instantiate()
- if current_level == 0:
- l.connect("level1_finished", _on_level1_finished)
- call_deferred("add_child", l)
-
- func _on_level1_finished():
- for n in get_children():
- remove_child(n)
- n.queue_free()
-
- current_level += 1
- call_deferred("add_child", levels[current_level].instantiate())
- ```
- You will also need to change the code in the `_on_timer_timeout()` function in the Killzone's script to `get_tree().get_root().get_node("Game").reload_current_level()` keeping in mind that "Game" is the name of the node in your game scene and is case sensitive. Now when you reach the end of Level 1, it will load Level 2 and you can play it.
- :::tip
- If you want to add further levels, don't forget to emit a finished signal from the level and load in and connect the new level and signal in the Game script.
- :::
-
-
-
-## Next Steps
-
-Congratulations, you now have a working 2D platformer. Here is a list of possible extensions you might want to add to your game:
-1. Investigate adding moving platforms using AnimationPlayers
-2. Killing enemies if you jump on them or some form of combat
-3. On screen health or life system
-4. On screen timer or score
-5. Game over screen
-6. Messing with tile collisions to create secret areas
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/2dracinggame.mdx b/src/content/docs/game-design/godot/2dracinggame.mdx
deleted file mode 100644
index 1a5fb6f9..00000000
--- a/src/content/docs/game-design/godot/2dracinggame.mdx
+++ /dev/null
@@ -1,150 +0,0 @@
----
-title: 2D Racing Game
-description: This page is a step by step guide to making a 2D platformer
-sidebar:
- order: 3
----
-
-This is a tutorial for a basic 2D racing game in Godot. This will follow on from previous learning we did in the [2D Platformer](/game-design/godot/2dplatformergame). We will be starting with a new project.
-
-Please download the asset pack linked [here](https://www.kenney.nl/assets/racing-pack) (it is free so you don't need to pay).
-
-import { Steps } from '@astrojs/starlight/components';
-
-## Making the Car
-
-
-
-1. Make a CharacterBody2D, then give it 2 child nodes: Sprite2D and CollisionShape2D.
-
-2. On sSprite2D, drag and drop spritesheet_vehicles.png into the empty texture in the inspector.
-
-3. In the region drop down, enable the region, click on edit region, and then cover a single car using the red dots.
- ![Screenshot showing edited region](/src/assets/godot/2DRacing/image1.png)
-
-4. For the Collisionshape2D, select new CapsuleShape2D and cover the car.
-
-5. Attach a script to CharacterBody2D and replace the code with this:
- ```gdscript
- extends CharacterBody2D
-
-
- const ACC = 25.0
- const MAX = 750
- const DEC = 5
- var motion = 0
-
-
- func _physics_process(delta: float) -> void:
- # Add the gravity.
-
- # Get the input direction and handle the movement/deceleration.
- # As good practice, you should replace UI actions with custom gameplay actions.
- var direction := Input.get_axis("ui_left", "ui_right")
- if direction:
- rotation += 2 * direction * delta
-
- var moving := Input.get_axis("ui_up", "ui_down")
- if moving:
- motion += ACC * moving
- if motion > MAX:
- motion = MAX
- elif motion < -MAX:
- motion = -MAX
-
-
- velocity = lerp(motion * transform.y, Vector2.ZERO, 0.2)
-
-
- if velocity != Vector2.ZERO:
- if motion > 0:
- motion = max(motion - DEC, -ACC)
- elif motion < 0:
- motion = min(motion + DEC, ACC)
-
- move_and_slide()
- ```
- This will allow your car to turn, accelerate, and decelerate.
-
-6. To test this, create a new scene and give it a Node2D, give it a child node Camera2D and your car scene as a child node. If you now play the track scene, you should be able to drive around.
- :::tip
- Remember to name your scenes. I named mine car and track1.
- :::
-
-
-
-## Making the Track
-
-
-
-1. Give track1 a TileMapLayer and name it Foreground. This is where we'll make the track.
-
-2. For the tileset, open the spritesheet_tiles.png. in the TileSet menu, set the texture region to 128 by 128 and in the inspector click on TileSet and change the tile size to 128 by 128.
- ![Screenshot showing TileSet size menu](/src/assets/godot/2DRacing/image2.png)
-
-3. Now you can draw your own track.
-
-4. If you want to make a background, create a new TileMapLayer by duplicating the foreground, and draw using that as your base.
- ![Screenshot showing current track](/src/assets/godot/2DRacing/image3.png)
-
-
-
-## Making the Second Car
-
-
-
-1. Duplicate the car scene and name it car2.
-
-2. In Project > Project Settings > Input Map, we will create other keys for the second player to use so that both players can use one keyboard. In the "add new action" box, type in something like Left2 and then click add.
-
-3. On the right, click the plus button and and then press a key to represent it. E.g. for left you could use a, right is d, backward is s, and forward is w. Repeat the steps for all 4 directions.
- ![Screenshot showing Input Map](/src/assets/godot/2DRacing/image4.png)
-
-4. Now go into the car2 script and change the `"ui_left"`, `"ui_right"` etc. to the inputs you made before. Now you can use the second car on the same keyboard!
-
-
-
-## Making the Menu
-
-
-
-1. Make a new scene with a Control node. Add a Label child node. In the inspector will be a text box - write Racing Game or whatever you want to call it.
-
-2. In the control section of the inspector there is a dropdown for theme overrides. This is where you can get creative and change the colors, the fonts, outlines etc. Save this scene as main.
- ![Screenshot showing creative controls](/src/assets/godot/2DRacing/image5.png)
-
-3. Make a new scene and give it a Button node. Change the text the same way as the Label in the previous step. Type in Level 1 or whatever you want to call it. You can change the style a similar way to the text with theme overrides, but now there is an extra styles menu. You can customise the button to look however you like.
-
-4. Attach a script to the button. In the node menu, connect the Pressed signal to the script and change the code to this:
- ```gdscript
- extends Button
-
- @export var level : String
- # Called when the node enters the scene tree for the first time.
- func _ready() -> void:
- pass # Replace with function body.
-
-
- # Called every frame. 'delta' is the elapsed time since the previous frame.
- func _process(delta: float) -> void:
- pass
-
-
- func _on_pressed() -> void:
- get_tree().change_scene_to_file(level)
- ```
-
-5. Now in your menu script you can add in the button for track one. In the inspector near the top you should see a text box for Level. Right click on your track1.tscn and copy the path and paste it into that text box.
- ![Screenshot showing level variable](/src/assets/godot/2DRacing/image6.png)
-
-6. Now when you run the menu (main) and click the button, it should load the track and you can now play it. To add more levels, simply add another button and change the path in the Level text box.
-
-
-
-## Next Steps
-
-1. Add more levels and connect them to your menu
-2. Add items to pick up
-3. Make an end screen
-4. Make your menus more sophisticated - how could you add a settings menu?
-5. Make a character selection menu
diff --git a/src/content/docs/game-design/godot/3dracinggame/0-main-scene/index.mdx b/src/content/docs/game-design/godot/3dracinggame/0-main-scene/index.mdx
new file mode 100644
index 00000000..8c41c33b
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/0-main-scene/index.mdx
@@ -0,0 +1,55 @@
+---
+type: tutorial
+unitTitle: "Making a 3D Racing Game"
+title: "Making the main scene and setup"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+import { Steps } from '@astrojs/starlight/components';
+
+# Preview
+:::note[If you're a beginner...]
+Read through and implement [3D Game](/game-design/godot/3dgame) or [3D Intro](/game-design/godot/3d) until you know the basics of how 3D works in Godot.
+:::
+![Preview of end result](/src/assets/godot/3DRacing/endResultPreview.png)
+
+You will be making a 3D racing game with a track, timer, lap counter, menu, and (of course) a race car!
+
+# Setting up the main scene
+
+
+1. Select **3D Scene** and rename to "Game".
+2. Add a child **MeshInstance3D** and rename to "Ground". In the Inpector:
+1. Select **New BoxMesh** under Mesh.
+2. Click the BoxMesh and change the Size to (x: 50, y: 1, z: 50).
+2. Still in the BoxMesh, under Material, select **New StandardMaterial3D**.
+2. Find a road picture (preferably .png) so you can see where the car is going.
+ - Download it into your racing game folder (I found mine here, on [OpenGameArt](https://opengameart.org/content/cartoony-montainside-ii)).
+ - In the Grounds' **Inspector > Mesh > Material > Albedo > Texture**, click 'Load' and select your texture.
+ - You will also need to change this Size to (x: 50, y: 1, z: 50).
+ ![Picture of Ground Mesh properties](/src/assets/godot/3DRacing/groundMeshProperties.png)
+ - To make the texture repeat itself: **UV1 > Triplanar** to 'On'.
+ - To make the texture not repeat as much, you can toggle with the **UV1 > Scale** properties.
+3. When on the Ground node, the menu bar will have a **Mesh** button.
+
+ Create a collision shape and change **Collision shape placement** to `Static Body Child`.
+4. Save your scene.
+
+
+### Current Look
+Your ground should now have repeated your picture and look something like this:
+
+
+![Picture of repeating picture on the ground](/src/assets/godot/3DRacing/repeatingAPicForGround.png)
+
+
+
+## Checklist
+
+- [ ] I have imported and used a ground texture.
+- [ ] I have a Game node and a Ground node.
+- [ ] I'm ready to make the car!
+
+
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/3dracinggame/1-making-the-car/index.mdx b/src/content/docs/game-design/godot/3dracinggame/1-making-the-car/index.mdx
new file mode 100644
index 00000000..9b7f26c8
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/1-making-the-car/index.mdx
@@ -0,0 +1,73 @@
+---
+type: tutorial
+unitTitle: "Making The Car"
+title: "Vehicle Wheels and Body"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+import { Steps } from '@astrojs/starlight/components';
+
+# Adding in your car
+
+:::note[Godot Documentation]
+Godot Documentation for nodes discussed in this section:
+[VehicleBody3D](https://docs.godotengine.org/en/stable/classes/class_vehiclebody3d.html#vehiclebody3d), [VehicleWheel3D](https://docs.godotengine.org/en/stable/classes/class_vehiclewheel3d.html)
+:::
+
+Click on the '+' button to create a new scene, and name it `car.tscn`.
+
+### The Vehicle Body
+
+1. Add a child **VehicleBody3D** node.
+
+ Change its **Mass** to **200kgs**.
+1. Add a **MeshInstance3D** and make a **New BoxMesh**. Change this to a car shape (my Size is (1.5, 0.5, 3)).
+1. On the menu bar **Mesh** button, create a collision shape. But this time,
+
+ **Collision shape placement** is `Sibling`. This is because the car needs to not be static.
+
+ **Collision Shape Type** is `Simplified Convex`.
+1. To change the colour of your car, go **MeshInstance3D > Mesh > Material > New StandardMaterial3D > Albedo** and change the colour.
+1. Save your scene.
+
+
+In your main scene with the ground, drag your saved car scene into the Ground (the top) node.
+
+#### Current Look
+
+![VehicleBody3D with colour](/src/assets/godot/3DRacing/VehicleBodySimple.png)
+
+
+### The Vehicle Wheels
+
+1. Add a child **VehicleWheel3D** node to VehicleBody3D and rename it to "FrontLeftWheel".
+1. Using the X, Y, Z axes adjust the wheel location to be just outside the vehicle body.
+1. In the Inspector:
+ - Under **VehicleBody3D Motion** tick
+ - [x] **Use as Steering** (as we want the front wheels to steer).
+ - Under **Wheel** change the **Roll Influence** to 1.0 (to keep the car from flipping over) **Radius** to 0.4m and **Friction Slip** to 4 (1 is normal grip, higher means more grip).
+ - Under **Suspension** set **Stiffness** to 50 (or a value between 50 and 100 for a race car).
+ - Under **Damping** set **Compression** to 0.5 (0.0 will make the car bounce through its' springs, we want less bounce for a race car).
+1. Add a child **MeshInstance3D** node and create a **New CylinderMesh**.
+ - By clicking on the mesh instance, change the Top & Bottom Radius to the same as the Wheel Radius (0.4m).
+ - Change the Height to 0.3m.
+ - Under the **MeshInstance3D > Tranform**, set the **Rotation z** to **90** degrees.
+1. Duplicate this wheel (CTRL/CMD D). Under the Transform tab, change the **x-position** to the negation of the current number. Rename it to "FrontRightWheel".
+1. Select both these wheels and duplicate again, this time changing the **z-position** to its' negation. Rename them to "BackRightWheel" and "BackLeftWheel".
+1. Set the back wheels' **VehicleBody3D Motion** to tick
+ - [x] **Use as Traction** (as we want the back wheels to accelerate).
+
+
+
+#### Current Look
+![VehicleBody3D with wheels and colour](/src/assets/godot/3DRacing/VehicleWheelsSimple.png)
+
+
+## Checklist
+
+- [ ] I have a car body and four wheels.
+- [ ] I understand how I can change the VehicleBody3D and VehicleWheel3D in the Inspector to suit my liking (e.g., if I want my wheels to be a different colour, or if I want the front wheels to accelerate too).
+
+
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/3dracinggame/2-car-controls/1.mdx b/src/content/docs/game-design/godot/3dracinggame/2-car-controls/1.mdx
new file mode 100644
index 00000000..3116557c
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/2-car-controls/1.mdx
@@ -0,0 +1,92 @@
+---
+type: tutorial
+title: "Driving the car"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+import { Steps } from '@astrojs/starlight/components';
+
+## Adding Controls
+
+To drive the car, you'll need to map these four inputs:
+1. "Forward" -> W key.
+1. "Backward" -> S key.
+1. "Left" -> A key.
+1. "Right" -> D key.
+
+:::note
+If you don't know how to do this, refer to [this 3D game resource](/game-design/godot/3dgame#changing-the-controls).
+:::
+## Adding a Script
+
+You'll need to add a **script** onto your **VehicleBody3D** so you can customise and control your car.
+
+```gdscript title="car.gd"
+extends VehicleBody3D
+
+# Constants that control how the car behaves
+var STEER_FORCE = 0.3 # Controls how strongly the car will steer left or right
+var SPEED = 500 # Controls the maximum driving speed of the car
+
+var max_torque = 500 # The force applied by the car's engine (acceleration)
+var max_rpm = 2500 # Maximum revolutions per minute (RPM) for the wheels
+
+var steer = 0.0 # Current steering angle (starts at 0, meaning no turn)
+
+# Called when the node is added to the scene (for initialization)
+func _ready() -> void:
+ pass
+
+# Called every frame, but we don't use it here since we're using physics
+func _process(delta: float) -> void:
+ pass
+
+# Called at every physics frame (handles the car's movement and turning)
+func _physics_process(delta: float):
+ # Get the player input for turning left and right
+ # 'Right' and 'Left' are inputs defined in the Input Map
+ # This gives us a value between -1 (full left) and 1 (full right)
+ var end_steer = Input.get_axis("Right", "Left") * STEER_FORCE
+
+ # Smoothly adjust the steering angle based on how much time (delta) has passed
+ # `lerp` smoothly moves `steer` toward `end_steer` at a speed of `steer_speed`
+ var steer_speed = 5 * delta
+ steer = lerp(steer, end_steer, steer_speed)
+
+ # Apply the calculated steering value to the car's wheels
+ set_steering(steer)
+
+ # Get the player input for accelerating and decelerating
+ # 'Backward' and 'Forward' are also inputs defined in the Input Map
+ # This gives us a value between -1 (full reverse) and 1 (full forward)
+ var acceleration = Input.get_axis("Backward", "Forward")
+
+ # Control the force applied to the left rear wheel
+ # The force decreases as the wheel reaches maximum RPM
+ var rpm = abs($BackLeftWheel.get_rpm()) # Get the current RPM of the wheel
+ $BackLeftWheel.engine_force = acceleration * max_torque * (1 - rpm / max_rpm)
+
+ # Same logic as the left wheel
+ rpm = abs($BackRightWheel.get_rpm())
+ $BackRightWheel.engine_force = acceleration * max_torque * (1 - rpm / max_rpm)
+
+```
+
+### Play the scene and drive the car!
+
+If the speed and steer force still doesn't feel like a race car, you can
+change the VehicleBody3D's mass, or all the VehicleWheel3D wheels, suspensions, or damping properties.
+
+If the Ground isn't big enough, set the scale to 2 (or however big you need it to be).
+
+
+## Checklist
+
+- [ ] My car can drive like a race car
+#### I know how to...
+- [ ] change my car to be slower
+- [ ] make the car take really sharp turns
+
+
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/3dracinggame/2-car-controls/2.mdx b/src/content/docs/game-design/godot/3dracinggame/2-car-controls/2.mdx
new file mode 100644
index 00000000..8e75f9a4
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/2-car-controls/2.mdx
@@ -0,0 +1,79 @@
+---
+type: tutorial
+title: "Extra: Follow Camera"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+import { Steps } from '@astrojs/starlight/components';
+
+## Follow Camera
+
+
+0. Delete your previous camera (if you have one).
+1. The current car that I have doesn't have any features which tell you if it's the front or back of the car.
+
+ So I have added some **MeshInstance3D**'s and chosen different shapes (on the **Mesh**'s down arrow) and colours (through **Material > Albedo**).
+ ![Picture of car with both front and back cameras with pizzazz](/src/assets/godot/3DRacing/addingPizzazzToCar.png)
+1. As you can see in the above photo, you will need to add these three nodes:
+ - **Node3D** (pivoting angle) as a child of your VehicleBody3D car.
+ - **Camera3D** (front view)
+ - **Camera3D** (reverse view)
+
+2. In the front view camera:
+
+ Turn the **Current** checkbox on, as the car always starts with the front facing camera.
+3. In the pivoting angle node:
+ :::note[Car Placement]
+ If you have placed your car in the world scene in a different spot than what you want, move your car to the **start / finish line**, because we will set the camera pivot node to the top level (and cause the cameras to not move with the car in the `world.tscn` scene).
+
+ If have already done this, move the car to the **start / finish line** and copy those (x y, z) coordinates into your `car.tscn` scene into the pivot camera control nodes coordaintes and on your car.
+ :::
+ Turn the **Top Level** checkbox on, so it doesn't inherit the cars' transformations (the positioning).
+4. We now need the front and back cameras to be facing both ends of the car.
+ Use the position and rotation under the **Transform** tab to do this, with the end result looking something like the photo above.
+5. Inside your car.gd script, you will need to adjust what the cameras look at as the car is running.
+
+ *Add* this code to your exisiting script.
+ ```gdscript title="car.gd"
+ # You can drag/drop the node into the script to get the references.
+ @onready var camera_pivot = $Node3DCameraPivot
+ @onready var camera_3d = $Node3DCameraPivot/Camera3D # Forward-facing camera.
+ @onready var reverse_camera = $Node3DCameraPivot/ReverseCamera
+ var camera_look_at # Variable to store where the cameras should look.
+
+ func _ready() -> void:
+ camera_look_at = global_position # Set initial camera look-at point to the car's current position.
+
+ func _physics_process(delta: float):
+ # Smoothly move the camera pivot towards the car's position based on speed and time
+ camera_pivot.global_position = camera_pivot.global_position.lerp(global_position, delta * 20.0)
+ # Smoothly align the camera pivot's orientation to match the car's orientation
+ camera_pivot.transform = camera_pivot.transform.interpolate_with(transform, delta * 5.0)
+ # Adjust where the camera should look at by adding car's linear velocity for a natural following effect
+ # Change the '2.0' around to see what's best for your car
+ camera_look_at = camera_look_at.lerp(global_position + linear_velocity, delta * 2.0)
+ # Point the forward and reverse cameras at the adjusted look-at position
+ camera_3d.look_at(camera_look_at)
+ reverse_camera.look_at(camera_look_at)
+ # Call function to check and update the camera view based on car direction
+ _check_camera_switch()
+
+ func _check_camera_switch():
+ # Check if the car is moving forward (dot product of velocity and forward direction > -1)
+ if linear_velocity.dot(transform.basis.z) > -1: # Change this number around to see what the best is for you
+ camera_3d.current = true # Use the forward-facing camera
+ else:
+ reverse_camera.current = true # Use the reverse camera for backing up
+
+ ```
+6. Test it out, and adjust any numbers that you see fit!
+
+
+
+## My car has a:
+
+- [ ] Smooth third person camera
+
+
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/3dracinggame/2-car-controls/index.mdx b/src/content/docs/game-design/godot/3dracinggame/2-car-controls/index.mdx
new file mode 100644
index 00000000..126f584e
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/2-car-controls/index.mdx
@@ -0,0 +1,42 @@
+---
+type: tutorial
+title: "Check In: Testing the Current Game"
+unitTitle: "Car Controls"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+
+## Adding a Camera & Light
+
+To view the current progress, you'll need to insert a **Camera3D** node as a child of your VehicleBody3D node.
+ :::tip[Positioning your camera]
+ Use the green, red, and blue arrows to move the camera to an appropriate area overlooking the car.
+ To see where you'll be driving put the camera just above the car, with the back wheels nearest to you.
+
+
+ Change the viewport angle (rotation x) to -20 degrees to make it look down. Make sure to save it!
+ :::
+If it's too dark, add a **DirectionalLight3D** and rotate it to face downwards.
+
+## Current File Structures
+
+This is what your car file structure should look like. Rename the nodes to simpler terms, e.g. VehicleBody3D -> "Car".
+
+![File structure for the car with camera](/src/assets/godot/3DRacing/builtCarFileStructure.png)
+
+What your world file structure should look like:
+![File structure for the world with light](/src/assets/godot/3DRacing/worldCurrentFileStructure.png)
+
+Now, when you play from your "Game" scene, you should see the car fall down and bounce into place.
+
+So far, you won't be able to control it -- this is on the next page.
+
+
+
+## Checklist
+
+- [ ] I'm ready to drive my car!
+
+
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/3dracinggame/3-track-building/1.mdx b/src/content/docs/game-design/godot/3dracinggame/3-track-building/1.mdx
new file mode 100644
index 00000000..35c6219f
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/3-track-building/1.mdx
@@ -0,0 +1,70 @@
+---
+type: tutorial
+title: "Track Barriers"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+import { Steps } from '@astrojs/starlight/components';
+
+# Adding in your barriers
+So the car doesn't go off track!
+
+
+1. ###### Making *a* barrier
+ Add another **CSGPolygon3D** and do the same steps as before [to make the path barrier visible](/game-design/godot/3dracinggame/3-track-building#making-the-path-visible).
+ - Then, check
+ - [x] **Path Joined** and the
+ - [x] **Use Collision** checkboxes.
+ - **Collision Priority** to `10` (as we don't want the car to get stuck into the barrier).
+2. Rename this polygon to `Invisible Barrier`, as it will be invisible soon!
+3. ###### Making the invisible barrier
+
+ We need the shape to have two rectangles following along the edges of the road.
+ - To make two rectangles from this, change the size to be 8, and set the x & y coordinates like in the photo.
+ - The two light-blue outlines you see in the photo should be the end result.
+ - You will not see the polygon take shape until you've entered in all the corner positions.
+ ![Invisible Road Barrier coordinates](/src/assets/godot/3DRacing/barrierPositionNumbers.png)
+ - Click the eye inside on the Road Barrier scene so that it isn't visible.
+ - Then test it out!
+5. ###### Creating your racing barrier bars
+
+ To make this look like a racing track, there are usually good looking race track barriers instead of the blocky barrier we've just made.
+ - Repeat the steps in [making *a* barrier](#making-a-barrier)
+ - Rename to `Road Barrier Left`, and add in these points:
+ ![track Barrier coordinates](/src/assets/godot/3DRacing/trackBarrierPositions.png)
+ - CTRL/CMD D to duplicate this barrier. Rename it to `Road Barrier Right` and try to change the points to be on the opposite side.
+
+ Answer
+ - Change corners 0 and 3 to have their x = -4, then corners 1 and 2 to have their x = -4.5, so it's inside the invisible barriers collision area.
+ - Change corners 0 and 1 to have their y = 1.5, then change corners 2 and 3 to have their y = 1, so the barrier is off the ground (around the cars height).
+
+ - To customise this track, repeat the steps of adding a texture (**NewStandardMaterial3D > Albedo > Texture**), and I've added **Smooth Faces** to make the barrier look round.
+6. ###### Merging the road & ground: a hill
+ - Repeat the steps in [making *a* barrier](#making-a-barrier)
+ - Rename to `Hill`.
+ - Add the same texture as the ground. [Previous instructions here (start at step 6).](/game-design/godot/3dracinggame/0-main-scene#setting-up-the-main-scene)
+ - Try to change the points to be from the bottom of the road to the top of the ground, all the way around.
+
+ Hint: how I did it
+ - Change corner 0's x = -20, corner 1's x = -6, corner 2's x = 6, and corner 3's x = 15.
+ - Change corners 0 and 3 to have their y = -10, then change corners 1 and 2 to have their y = -0.5.
+
+
+
+Test it out again!
+
+## Current look
+![Picture of track with hill and barriers on the ground](/src/assets/godot/3DRacing/trackPathResult.png)
+
+
+## Checklist: I've built...
+
+- [ ] An invisible barrier
+- [ ] A left & right race track barrier
+- [ ] A hill for the track.
+
+
+
+
diff --git a/src/content/docs/game-design/godot/3dracinggame/3-track-building/2.mdx b/src/content/docs/game-design/godot/3dracinggame/3-track-building/2.mdx
new file mode 100644
index 00000000..d71efe69
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/3-track-building/2.mdx
@@ -0,0 +1,296 @@
+---
+type: tutorial
+title: "Finish Line & Display"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+import { Steps } from '@astrojs/starlight/components';
+
+## Adding in some of your heads up display
+
+In order for us to know if we are at the finish line, we will need a display for the racer.
+Ideally, this should include a lap counter, time, a finished screen and a crashed screen.
+
+:::note[Godot Documentation]
+Godot Documentation for nodes discussed in this section:
+[Controls](https://docs.godotengine.org/en/stable/classes/class_control.html), [Labels](https://docs.godotengine.org/en/stable/classes/class_label.html#class-label), [Signals](https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html), and [Area3D](https://docs.godotengine.org/en/stable/classes/class_area3d.html).
+:::
+
+## Setting Up
+
+1. Add a new scene, and add in a **Control** node. Name it "HUD".
+
+ Under the **Layout > Anchors Preset** in the Inpsector, set this to **Full Rect**. This means the HUD will be the full size of the game screen.
+
+ Drag this `hud.tscn` scene into the `car.tscn` scenes' top node (which should be **VehicleBody3D**).
+2. In the HUD scene, add a **Panel** node. This will be the top display bar, showing the timer, laps, and if the car is reversing.
+
+ :::note[2D views in a 3D game]
+ You will need to go into the 2D view to be able to see the **Panel** and our future **ColorRect** nodes.
+ This is because this layer will sit right at the front of all the other objects, and won't interfere with any of the cars' 3D nodes.
+ This means in each of your third person cameras, this "HUD" will constantly sit at the front of the display.
+ :::
+
+ Change the sizing of this panel using the red dot anchors to be sitting on the top of the screen with full width.
+ So we can see behind this panel: in **CanvasItem > Visibility > Modulate**, change the alpha (how transparent the object is) to halfway. And change the colour to your favourite colour!
+0. Accessing our future labels
+
+ :::note[Global Groups]
+ A global group is a way to organize nodes dynamically under a label that you can access from anywhere in your project.
+ This is found under the **Node** tab as **Groups**, where you can add nodes to a specific group name, enabling a powerful way
+ to manage, interact with, and send commands to multiple nodes at once.
+ :::
+
+ Add a global group called `labels`, so that we can change each of the lap, timer and reverse labels in the scripts.
+
+
+## Adding laps with a finish line
+
+0. *In the `hud.tscn` scene:* Add a **Label** into the **Panel** node.
+ - Rename it to `Laps`.
+ - Set the text to `Laps: 1/3`.
+ - Change the **scale** to `2`, to make it more visible.
+
+1. Move it to the top left corner of the screen.
+
+2. Add the `labels` group to the `Laps` label, you should see a rectangle with a circle inside it pop up next to its name in the scene bar.
+3. *In the `track_1.tscn` scene:* Add a script on the **Node3D**.
+3. ##### Making the finish line
+ Create these nodes under the **Node3D** (the root).
+ - **Area3D** (named "FinishLine")
+ - **CollisionShape3D** (with a shape of **BoxShape3D**)
+ - **MeshInstance3D** (with its mesh being **BoxMesh** and having a size of `x=8.5, y=1, z=2`)
+ - Add a new **StandardMaterial3D** with a finish line picture like this (using the **Albedo** texture).
+
+ ![Finish line picture to add into MeshInstance3D](/src/assets/godot/3DRacing/finishLine.png)
+
+ You will need to play around with the **UV1**'s scale. The numbers I ended up with was `x=3, y=2.02, z=0`.
+
+ Add a signal to "FinishLine" **Area3D** check whether the car has exited the area (so we will be able to check if the car has done a lap).
+ To do this, you need to go in **Node > Signals** tab on the right, press `body_exited(body: Node3D)`, and click `Connect` down the bottom.
+
+4. Inside our newly created script, add this code to update the laps each time. Make sure to read and understand the code!
+ ```gdscript title="track_1.gd"
+ # Variables to manage laps
+ var maxLaps = 3 # Total laps to complete the race
+ var currentLap = 1 # Current lap the vehicle is on
+
+ # Variable to store references to all labels in the "labels" group
+ var labels
+
+ # Called when the node enters the scene tree for the first time
+ func _ready() -> void:
+ # Get all nodes in the "labels" group
+ labels = get_tree().get_nodes_in_group("labels")
+
+ # Called when the vehicle exits the finish line area (signal from finish line Area3D)
+ func _on_finish_line_body_exited(body: Node3D) -> void:
+ if body is VehicleBody3D: # Check if the body that exited is the vehicle
+ # Check if the forward camera is active (not reversing into finish line)
+ if body.camera_3d.current == true:
+ # Check if the car hasn't finished
+ if currentLap != maxLaps:
+ # Increase lap count, reset checkpoint flag, and update the lap label
+ currentLap += 1
+ var labelLaps = labels.filter(func(label): return label.name == "Laps")[0]
+ labelLaps.text = "Laps: " + str(currentLap) + "/" + str(maxLaps)
+ ```
+5. You will now be able to test it out, and each time you cross the finish line, the laps will increase!
+
+
+:::caution
+You will need to check the finish line Area3D is shaped for the width and height of the track and car.
+![Finish line Area3D to make sure that area is in track.](/src/assets/godot/3DRacing/area3dFinishLine.png)
+:::
+
+### Checkpoints
+We will need more precise lap counting than just checking if the car is facing forward. Checkpoints across the track will help keep count of the actual laps the car has done.
+
+1. Add the same nodes (*apart* from the MeshInstance3D) and a signal like the [finish line's](#making-the-finish-line).
+
+ Name the **Area3D** "Checkpoint".
+
+ These checkpoints will be invisible. You will need to put them around the track. For the sake of time, I have only added one 'halfway' checkpoint, but it is recommended to do more.
+2. Then add this code to the connected function from the signal you made.
+ ```gdscript title="track_1.gd"
+ var passed_checkpoint = false # Flag to ensure vehicle passes a checkpoint before completing a lap
+ # (exisiting code here)
+
+ func _on_finish_line_body_exited(body: Node3D) -> void:
+ if body is VehicleBody3D:
+ # Check if the forward camera is active (no reversing into finish line) AND checkpoint has been passed
+ if body.camera_3d.current == true && passed_checkpoint:
+ # Check if the car hasn't finished
+ if currentLap != maxLaps:
+ # (exisiting code here)
+ passed_checkpoint = false # Reset checkpoint flag for the next lap
+
+ # Called when the vehicle exits a checkpoint area (signal from checkpoint Area3D)
+ func _on_checkpoint_body_exited(body: Node3D) -> void:
+ # Check if the vehicle is the one triggering the checkpoint and the main camera is active
+ if body is VehicleBody3D && body.camera_3d.current == true:
+ passed_checkpoint = true # Mark checkpoint as passed, allowing lap completion
+ ```
+3. Now, if you turn around before passing this checkpoint and go through the finish line, the lap counter won't increase!
+
+
+## Adding a timer
+
+1. To make a race car timer, we will need milliseconds, seconds, and minutes.
+2. *In the `hud.tscn` scene:* Copy and paste the 'Laps' label.
+ Rename it to `Time` and move it to the top right hand corner of the screen.
+ Set the text to `Time: 00:00.000`.
+3. Add this code to your existing code.
+ If you understand the comments, feel free to delete them to reduce script clutter.
+ ```gdscript title="track_1.gd"
+ # Variables for timer
+ var time = 0.0 # Total time elapsed since the start of the race in seconds
+ var minutes = 0 # Minutes component of the time display
+ var seconds = 0 # Seconds component of the time display
+ var msec = 0 # Milliseconds component of the time display
+
+ # Called every frame, where 'delta' is the elapsed time since the previous frame
+ func _process(delta: float) -> void:
+ # Update the total time by adding delta (time since last frame)
+ time += delta
+
+ # Calculate milliseconds, seconds, and minutes for time display
+ msec = fmod(time, 1) * 100
+ seconds = fmod(time, 60)
+ minutes = fmod(time, 3600) / 60
+
+ # Format the time as a string (MM:SS.mmm) for display
+ var timeString = "%02d:%02d.%03d" % [minutes, seconds, msec]
+
+ # Find the label displaying the time (assumes a label named "Time" is in the labels group)
+ var labelTime = labels.filter(func(label): return label.name == "Time")[0]
+ labelTime.text = timeString
+ ```
+4. Well done! You now have a working timer for your racing. Now you can see who the fastest race car driver is!
+
+
+### Adding in a reverse state (optional)
+
+1. If our car is still quite symmetrical, it would be easier to add in a reversing label so we can see if we are reversing or not.
+2. *In the `hud.tscn` scene:* Copy and paste the 'Laps' label.
+ Rename it to `Reversing` and move it next to the Laps label.
+ Set the text to `Reversing...`.
+ Set the colour to red.
+ Uncheck the **Visible** (or the eye icon), as we only want it to show up when we reverse.
+3. In our `car.gd` script, we will be able to put this label inside the script using our global group `labels`.
+ ```gdscript title="car.gd"
+ var labels
+ var reverse_label
+ # (existing variables)
+
+ func _ready() -> void:
+ camera_look_at = global_position
+ # Find our labels group, and filter them until we've found our reverse label
+ labels = get_tree().get_nodes_in_group("labels")
+ reverse_label = labels.filter(func(label): return label.name == "Reversing")[0]
+
+ # (existing functions)
+
+ func _check_camera_switch():
+ if linear_velocity.dot(transform.basis.z) > -1:
+ camera_3d.current = true
+ reverse_label.hide() # We're driving forward
+ else:
+ reverse_camera.current = true
+ reverse_label.show() # We're reversing!
+ ```
+
+
+## Making the finished state
+
+
+1. Once we've finished the race, we celebrate with a finish screen!
+2. *In the `hud.tscn` scene:* Add a **ColorRect** node under the main **Control** node.
+ - Rename it to `FinishedLevel` and change it's size to the whole screen.
+ - Change the colour to transparent(ish) green.
+ - Uncheck the **Visible** (or the eye icon), as we only want it to show up when we finish the track.
+ - Add it to the `labels` global group (in **Node > Signals**).
+ - Add a **Label** as a child of **ColorRect** and set the text to `You've finished the track!`. Set the **scale** to 3.
+3. Inside our `track_1.gd`, make some changes to our `_on_finish_line_body_exited` function, and add in a `stop` function.
+
+ ```gdscript title="track_1.gd"
+ func _on_finish_line_body_exited(body: Node3D) -> void:
+ if body is VehicleBody3D: # Check if the body that exited is the vehicle
+ # Check if the main camera is active and checkpoint has been passed
+ if body.camera_3d.current == true && passed_checkpoint:
+ # Check if the current lap is the last one
+ if currentLap == maxLaps:
+ # Display "Finished Level" label to indicate race completion
+ var finishedLevel = labels.filter(func(label): return label.name == "FinishedLevel")[0]
+ finishedLevel.show()
+ stop() # Stop processing further laps
+ else:
+ # Increase lap count, reset checkpoint flag, and update the lap label
+ currentLap += 1
+ var labelLaps = labels.filter(func(label): return label.name == "Laps")[0]
+ labelLaps.text = "Laps: " + str(currentLap) + "/" + str(maxLaps)
+ passed_checkpoint = false # Reset checkpoint flag for the next lap
+
+ # Function to stop the timer and prevent further processing
+ func stop() -> void:
+ set_process(false)
+ ```
+
+
+## Making the crashed state
+
+
+1. *In the `hud.tscn` scene:* Add a **ColorRect** node under the main **Control** node.
+ - Rename it to `CrashedScreen` and change it's size to the whole screen.
+ - Change the colour to transparent(ish) red.
+ - Uncheck the **Visible** (or the eye icon), as we only want it to show up when we crash.
+ - Add it to the `labels` global group (in **Node > Signals**).
+ - Add a **Label** as a child of **ColorRect** and set the text to `You crashed! Restart the game.`. Set the **scale** to 3.
+3. Inside our `car.gd`, make some changes to our `_physics_process` and `_ready` functions.
+
+ ```gdscript title="car.gd"
+ var crash_timer = 0.0
+ var crash_threshold = 3.0 # Time in seconds before considering it crashed
+ var crash_screen
+
+ func _ready() -> void:
+ # (exisiting code)
+ crash_screen = labels.filter(func(label): return label.name == "CrashedScreen")[0]
+
+ func _physics_process(delta: float):
+ # (exisiting code)
+ # if the car has crashed: Check if the vehicle is upside down
+ if transform.basis.y.dot(Vector3.UP) < 0:
+ crash_timer += delta
+ if crash_timer >= crash_threshold:
+ crash_screen.show()
+ else:
+ crash_timer = 0.0 # Reset timer if not upside down
+
+ ```
+
+
+## Current File Structures
+The heads up display file structure:
+![HUDFileStructure](/src/assets/godot/3DRacing/HUDFileStructure.png)
+
+
+The track 1 file structure:
+![track1fileStructure](/src/assets/godot/3DRacing/track1fileStructure.png)
+
+
+
+
+
+## I've made...
+
+- [ ] A race timer
+- [ ] A reverse state
+- [ ] A finished state
+- [ ] A crashed state
+- [ ] A lap counter
+- [ ] A finish line
+
+
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/3dracinggame/3-track-building/3.mdx b/src/content/docs/game-design/godot/3dracinggame/3-track-building/3.mdx
new file mode 100644
index 00000000..0041f0dd
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/3-track-building/3.mdx
@@ -0,0 +1,93 @@
+---
+type: tutorial
+title: "Extra: Boost"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+import { Steps } from '@astrojs/starlight/components';
+
+## Boost
+
+If you want a little extra help with speed, here we go!
+
+
+1. *In the `hud.tscn` scene:* Add a **ColorRect** node under the main **Control** node.
+ - Rename it to `Boost` and change it's size to an upright rectangle on the right side of the screen.
+ - Change the colour to transparent(ish) dark green.
+ - Add another **ColorRect** node under the main **ColorRect** `Boost` node. Rename this to `BoostIndicator`.
+ - Add `BoostIndicator` to the `labels` global group (in **Node > Signals**).
+ - Add a **Label** as a child of the `Boost` node and set the text to `Boost`. Set the **scale** to 1.5.
+2. Bind a 'Boost' to your Space Bar in the **Settings > Input Map**.
+3. Inside our `car.gd`, add in these changes.
+
+ ```gdscript title="car.gd"
+ var boost_multiplier = 1.5 # Factor to increase acceleration during boost
+ var boost_duration = 2.0 # Boost lasts for 2 seconds
+ var boost_cooldown = 5.0 # 5 seconds cooldown between boosts
+ var is_boosting = false # Tracks if boost is active
+ var boost_timer = 0.0 # Timer to track boost duration
+ var cooldown_timer = 0.0 # Timer for boost cooldown
+ var boost_rect_size = 370 # Change this to your rectangles current size!
+ var boost_indicator
+
+ func _ready() -> void:
+ # (exisiting code)
+ boost_indicator = labels.filter(func(label): return label.name == "BoostIndicator")[0]
+
+ func _process(delta: float) -> void:
+ # Handle boost activation with space bar
+ if Input.is_action_just_pressed("Boost") and cooldown_timer <= 0.0:
+ start_boost()
+
+ # Update timers if boosting or in cooldown
+ if is_boosting:
+ boost_timer -= delta
+ # the lerp function maps the timer to the boost bar.
+ boost_indicator.size.y = lerp(0, boost_rect_size, boost_timer / 2.0)
+ if boost_timer <= 0.0:
+ end_boost() # End boost when timer runs out
+ elif cooldown_timer > 0.0:
+ cooldown_timer -= delta
+ boost_indicator.size.y = lerp(boost_rect_size, 0, cooldown_timer / 5.0)
+ boost_indicator.color = Color('ca7566a5') # transparent red
+ else:
+ boost_indicator.color = Color('caff66a5') # transparent green
+
+ func _physics_process(delta: float):
+ # steering & speed physics
+ # (exisiting code)
+ var acceleration = Input.get_axis("Backward", "Forward")
+ # Add a check if the car is boosting between acceleration declaration
+ # and before the rpm declaration
+ if is_boosting:
+ acceleration *= boost_multiplier # Apply boost multiplier
+
+ var rpm = abs($BackLeftWheel.get_rpm())
+ # (exisiting code)
+
+ func start_boost():
+ is_boosting = true # Activate boosting
+ boost_timer = boost_duration # Set the boost duration timer
+ cooldown_timer = boost_cooldown # Set the cooldown timer
+
+ func end_boost():
+ is_boosting = false # Deactivate boosting
+ ```
+
+
+You should now have something like this:
+When pressing space bar:
+![boostGameRunning](/src/assets/godot/3DRacing/boostGameRunning.png)
+
+When the boost is reloading:
+![boostReloadingGameRunning](/src/assets/godot/3DRacing/boostReloadingGameRunning.png)
+
+
+
+## Checklist
+
+- [ ] I've built a boost bar!
+
+
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/3dracinggame/3-track-building/index.mdx b/src/content/docs/game-design/godot/3dracinggame/3-track-building/index.mdx
new file mode 100644
index 00000000..d5cd6c0f
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/3-track-building/index.mdx
@@ -0,0 +1,79 @@
+---
+type: tutorial
+title: "Track Building"
+unitTitle: "Building a Track"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+import { Steps } from '@astrojs/starlight/components';
+
+# Adding in your track
+:::note[Godot Documentation]
+Godot Documentation for nodes discussed in this section:
+[Path3D](https://docs.godotengine.org/en/stable/classes/class_path3d.html), [CSGPolygon3D](https://docs.godotengine.org/en/stable/classes/class_csgpolygon3d.html)
+:::
+Click on the '+' button to create a new scene, and name it `track_1.tscn`.
+
+
+1. Add a **Node3D**.
+2. Add a **Path3D** to **Node3D**.
+3. In your main scene with the ground, drag this saved track scene into the Ground (the top) node. You might notice that the path is below (or in line with) the ground. You will need to *decrease the y-position* of the ground.
+4. Back in your track scene, you'll notice that **Path3D** has extra options in the navigation bar. Hover over each of them to get an understanding of what their functions are.
+ ![Picture of Path3D controls](/src/assets/godot/3DRacing/Path3DControls.png)
+ - Click on the green **Add point** button.
+ - Click on the Y Axis (the top right coloured axes) so that each of these points will be at the same height.
+ - Add your points.
+ - To finish, click the blue triangle **Close curve** button.
+5. ###### Making the path curved
+ - Click the **Select control points**.
+ - Hold shift and click each point to drag the control handles around.
+ - It should something like this inside the world scene:
+ ![Picture of curved path on the ground](/src/assets/godot/3DRacing/curvedPath3D.png)
+6. ###### Making some hills
+
+ Move your path points up using the green up arrow.
+ ![Picture of moving path point up](/src/assets/godot/3DRacing/editingPathPointHeights.png)
+7. ###### Making the path visible
+ - Add **CSGPolygon** to Path3D.
+ - Set the **Mode** to Path.
+ - Assign the **Path Node** to the Path3D.
+8. ###### Changing the shape of the path
+ - Click the Polygons' **PackedVector2Array** and you will see size 4 (we need 4 corners to make a rectangular path).
+ :::note[Remember]
+ The y will change the height of the path, and x will change the width of the path.
+ :::
+ These are the numbers I have entered in:
+ ![PackedVector2Array's specific points](/src/assets/godot/3DRacing/pathMeasurements.png)
+
+ Explanation
+ - If you change corners 0 and 1 to have their x = -5, then corners 2 and 3 to have their x = 5, you will see the path widen.
+ - If you leave corners 0 and 3 to have their y = 0, then change corners 1 and 2 to have their y = 0.1, the path will be thinner.
+
+
+ - Now, move the car onto the track to see if the sizing of the path is good. If the sizing is bad, change the x and y to be wider or thicker, respectively.
+9. ###### Tilting the path (if your end points haven't joined together properly)
+ - ![photo of how to tilt](/src/assets/godot/3DRacing/tiltButton.png)
+ - On Path3D, select the yellow circle button (which will then turn green) and then use the dots on each point to change each points' tilt.
+ - If you see any points in the path which doesn't look 'smooth' enough, change the tilt.
+10. In the CSGShape3D section, tick the **Use Collision** checkbox so the car can drive on the path.
+11. ###### Creating a road-like texture
+ - Inside the **CSGPolygon3D**, make a new **StandardMaterial3D** under **Material**.
+ - Download this road picture [from OpenGameArt](https://opengameart.org/content/golgotha-textures-tunnelroadjpg). Rotate the picture.
+ - Adjust the **Scale** under **UV1** to fit your path.
+ It should look something like this (some path points have been deleted to make this first track easy).
+ ![photo of the path with a road picture as a texture, and less joining points](/src/assets/godot/3DRacing/pathCurrentLookWithRoad.png)
+
+11. Test it out and change the tilting, size, and height of the track if it's too hard to complete.
+
+
+
+## Checklist
+
+- [ ] I've made a road.
+- [ ] My car can drive on this road.
+- [ ] The road isn't too hard to drive on.
+- [ ] The track is easy for a '1st level' track.
+
+
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/3dracinggame/4-main-menu/1.mdx b/src/content/docs/game-design/godot/3dracinggame/4-main-menu/1.mdx
new file mode 100644
index 00000000..b9691713
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/4-main-menu/1.mdx
@@ -0,0 +1,31 @@
+---
+type: tutorial
+title: "Check Out: You've made a 3D racing game!"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+import Checklist from '/src/components/tutorial/Checklist.astro';
+import Box from '/src/components/tutorial/Box.astro';
+import { Steps } from '@astrojs/starlight/components';
+
+## Next Steps
+
+You've just made your very own 3D racing game! It's now all yours to add on new features and change it to your liking!
+Here's some ideas for what your next steps:
+
+
+1. Showing a speedometer in your HUD.
+2. Collect powerups or extra boosts as you drive along the track.
+3. Have a crack at multiplayer if you're feeling up for it!
+
+
+Thanks for following along this far! Happy Godoting and have fun.
+
+
+
+## Checklist
+
+- [ ] I have made a speedometer in my HUD.
+- [ ] There are powerups or extra boosts as I drive along the track.
+
+
\ No newline at end of file
diff --git a/src/content/docs/game-design/godot/3dracinggame/4-main-menu/index.mdx b/src/content/docs/game-design/godot/3dracinggame/4-main-menu/index.mdx
new file mode 100644
index 00000000..05baf3e0
--- /dev/null
+++ b/src/content/docs/game-design/godot/3dracinggame/4-main-menu/index.mdx
@@ -0,0 +1,300 @@
+---
+type: tutorial
+title: "Menu and Levels"
+unitTitle: "Menu and Levels"
+description: This page works through creating a full 3D racing game step-by-step
+---
+
+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:
+[User Interface](https://docs.godotengine.org/en/stable/tutorials/ui/index.html), [PackedScene](https://docs.godotengine.org/en/stable/classes/class_packedscene.html), [ButtonGroup](https://docs.godotengine.org/en/stable/classes/class_buttongroup.html), [SceneTree](https://docs.godotengine.org/en/stable/tutorials/scripting/scene_tree.html)(for changing the current scene via gdscript), and [VBoxContainer](https://docs.godotengine.org/en/stable/classes/class_vboxcontainer.html)(for the HUD pause menu button).
+:::
+## Adding in a title screen and new levels
+
+
+
+1. ###### Create a new scene and name it `main_menu.tscn`.
+ Add these nodes to the scene tree:
+ - A **Control** node and name it "Main Menu". Then add these nodes as children:
+ - A **Label** which will be your games' name.
+ :::tip[Improving your games' aesthetic]
+ If you want your labels and buttons to look nice, change the properties in `Theme Overrides`.
+
+ Take a look [on this link here](https://docs.godotengine.org/en/stable/tutorials/ui/gui_using_fonts.html) for further information.
+ :::
+ - A **TextureRect** (for photos) or **ColourRect** (for just a colour) which will be the background for your games main menu.
+ - A "Start Game" **Button**. You can also change the font/colour themes for the buttons too.
+ - A "Levels" **Button**.
+ - An "Exit" **Button**.
+
+ It could look something like this:
+ ![title screen photo with start game, level menu, and quit game buttons](/src/assets/godot/3DRacing/titleScreen.png)
+2. Change this scene to your main scene. Do this by clicking `Project > Project Settings > Application > Run` and changing the "Main Scene" to `main_menu.tscn`.
+3. Add a script to your "Main Menu" **Control** node. Leave this for now.
+4. Connect your three buttons when each "BaseButton" is pressed.
+
+ :::tip[Can't remember how to connect a signal?]
+ Click on the `Node` tab on the right hand sidebar, select `Pressed` (in this case) and click `Connect...` down the right hand side of the bar.
+ You should also see that these three functions come up in the code:
+ ```gdscript title="main_menu.gd"
+ func _on_start_game_pressed() -> void:
+ pass
+
+ func _on_exit_game_pressed() -> void:
+ pass
+
+ func _on_levels_pressed() -> void:
+ pass
+ ```
+ :::
+5. ###### Create a new scene and name it `levels_menu.tscn`.
+ Copy `CMD/CTRL C` and paste `CMD/CTRL V` all the `main_menu.tscn` nodes into this scene.
+ Change these nodes in the scene tree:
+ - Name the **Control** node "Levels Menu".
+ 1. Keep the **Label** the same.
+ 2. Keep the **TextureRect** or **ColourRect** the same.
+ 3. Remove the Signal connection from the "Start Game" **Button** and rename it to "Level 1".
+
+ Removing signal connection: Select the `Node > pressed()` function and `CMD/CTRL BACKSPACE` it.
+ 4. Remove the "Levels" **Button**.
+ 5. Remove the "Exit" **Button**.
+
+ It could look something like this:
+ ![levels menu photo with start game and level 1 to 3 buttons](/src/assets/godot/3DRacing/levelsMenuScreen.png)
+6. In the "Level 1" **Button**, check the `Inspector > BaseButton > Toggle Mode` to "On".
+7. In the same section, click the down arrow in the **ButtonGroup** and click `New ButtonGroup`.
+
+ Click the down arrow again and click "Save As..." and name it `levels.tres`.
+8. Copy and paste this **Button** according to the number of levels you want to have. I have done 3. Make sure each Level Button is in the `levels.tres` ButtonGroup!!
+9. Add a new script onto the root node of `levels_menu.tscn`. You might need to remove the previous script if you copy/pasted it from `main_menu.tscn`.
+10. Add a "Start Game" **Button** and connect a **pressed() Signal** to it. You should know how to do this.
+11. ###### Create a new track!
+
+ Create a new scene and name it `track_2.tscn`. In the code later on, you'll see why these tracks need the `track_[num].tscn` naming convention!
+12. Click `track_1.tscn` and click the top **Node3D**. Copy and paste this into your new track scene.
+
+ - You'll notice it pastes everything in there. In order to just change your new track without also changing the first track, you'll need to delete everything apart from the top node.
+ - After this, go into your `track_1.tscn` and copy everything underneath the top node. Paste this into your `track_2.tscn`. While it's still all selected, right-click these nodes and click **Access as Unique Name**.
+ - This should make each track unique, so that you can change the **Path3D** points without worrying about messing up your original track!
+13. Now change/add/remove your **Path3D** points and **Area3D** Checkpoint/s to create a harder or longer track.
+ - If you have spare time, feel free to change the textures for a different look on each track.
+ :::note[New Track Requirements]
+ You will need to keep the finish line / start line area the same in each track you make, as the car in the `world.tscn` will start in the same position each time.
+ :::
+ This is what my new track looks like!
+ ![new track photo](/src/assets/godot/3DRacing/newTrack.png)
+
+
+
+## Adding functionality to the levels menu and main menu
+So far, when you play these two scenes, nothing will happen.
+
+For the levels menu, players should be able to choose a level, and the script will load the appropriate track.
+If they pick the first level, the default world scene is used.
+For other levels, the script dynamically modifies the world to include the chosen track.
+
+Add this code to the `levels_menu.tscn` script.
+**Go through each line and read the comments to understand why it's there.**
+
+ GDScript Code for the levels menu
+ ```gdscript title="levels_menu.gd"
+ extends Control
+
+ # The 'levels.tres' button group, used for which level the player has selected.
+ @onready var button_group = $"Level 1".button_group
+
+ # Preloads the base world scene. This is the template for all levels.
+ @onready var world = preload("res://world.tscn") as PackedScene
+
+ func _ready() -> void:
+ pass
+
+ func _process(delta: float) -> void:
+ pass
+
+ # When the "Start Game" button is pressed.
+ func _on_start_game_pressed() -> void:
+ # Get the level number from the selected button's name (e.g., "Level 1").
+ var num = button_group.get_pressed_button().name.split(" ")[1]
+
+ # If the selected level is "1", just load the default world scene directly.
+ if (num == "1"): # Level 1 is already part of the base world scene.
+ get_tree().change_scene_to_packed(world) # Switch to the base world scene.
+ else:
+ # For levels other than 1, modify the world scene with the chosen track.
+ change_world_level(num)
+
+ # This function creates and loads a custom world for the selected level.
+ func change_world_level(num):
+ # Create an instance (a copy) of the base world scene to modify it.
+ var world_instance = world.instantiate()
+
+ # Check if there's already a track in the base world and remove it.
+ var track_1 = world_instance.get_node_or_null("Track") # Look for an existing track node.
+ if track_1:
+ world_instance.remove_child(track_1) # Detach the track node from the world.
+ track_1.queue_free() # Delete the old track from memory.
+
+ # Load the new track scene based on the chosen level number.
+ var new_track = load("res://track_" + str(num) + ".tscn") as PackedScene
+ var new_track_instance = new_track.instantiate() # Create an instance of the new track.
+
+ # Add the new track to the world instance.
+ world_instance.add_child(new_track_instance)
+ new_track_instance.name = "Track" # Name the new track so it can be identified later.
+ new_track_instance.owner = world_instance # Set ownership so the track is saved properly.
+
+ # Create a new PackedScene to store the modified world with the new track.
+ var new_world = PackedScene.new()
+ if new_world.pack(world_instance) == OK: # Try to "pack" the modified world into a new scene.
+ # Define a temporary file path to save the new world scene.
+ var temp_scene_path = "user://temp_world.tscn"
+
+ # Save the packed scene to the temporary file path.
+ ResourceSaver.save(new_world, temp_scene_path)
+
+ # Load the saved scene into the game.
+ get_tree().change_scene_to_file(temp_scene_path)
+ else:
+ # If packing the scene fails, print an error message for debugging.
+ print("Error: Could not pack the modified world scene!")
+
+ ```
+
+
+After you've read the comments and understand most of the code, feel free to delete it to reduce clutter.
+
+
+
+Now add this code to the `main_menu.tscn` script.
+This script is for the main menu of your racing game.
+It handles actions when players click buttons like Start Game, Choose Level, or Exit Game. Depending on what
+the player selects, the game either starts, shows the level selection screen, or exits.
+
+**Go through each line and read the comments to understand why it's there.**
+
+ GDScript Code for the main menu
+ ```gdscript title="main_menu.gd"
+ extends Control
+
+ # Preloads the default starting level scene. This is the first race track.
+ @onready var start_level = preload("res://world.tscn") as PackedScene
+
+ # Preloads the level selection menu scene. This screen lets the player pick a specific level.
+ @onready var choose_level = preload("res://levels_menu.tscn") as PackedScene
+
+ func _ready() -> void:
+ pass
+
+ func _process(delta: float) -> void:
+ pass
+
+ # This function is called when the "Start Game" button is clicked.
+ func _on_start_game_pressed() -> void:
+ # Switches the game to the default starting level.
+ get_tree().change_scene_to_packed(start_level)
+
+ # This function is called when the "Exit Game" button is clicked.
+ func _on_exit_game_pressed() -> void:
+ # Quits the game application.
+ get_tree().quit()
+
+ # This function is called when the "Choose Levels" button is clicked.
+ func _on_levels_pressed() -> void:
+ # Switches the game to the level selection menu.
+ get_tree().change_scene_to_packed(choose_level)
+
+ ```
+
+
+
+## Adding in a pause menu in the HUD
+
+If you want to pause the game and switch levels or restart your current game if you've crashed, we'll need a popup menu!
+
+
+Here's a preview of the end result:
+![new track photo](/src/assets/godot/3DRacing/pauseMenuHUD.png)
+
+
+The red lines indicate the new nodes that you will be adding into your HUD scene.
+
+
+1. Inside the `hud.tscn` scene, add a script to the root **Control** node. This will be for connecting signals for the various buttons - like restarting the game - and the functions to do this.
+1. Under your **Panel** which contains the "Laps", "Reversing", and "Time" labels, add a "Pause Race" **Button**.
+
+ Connect a "pressed()" signal via the `Node` tab on the sidebar.
+2. Turn the `Inspector > Visibility > Top Level` to "On", so you can still interact with the **Button** when the crashed and finished screens come up. I also changed the font size to `25px`.
+3. Add a **PanelContainer** to the root node of this scene. Center this on your HUD scene.
+3. ###### Adding the VBoxContainer
+
+ In the inspector, you can change the alignment to "Center".
+4. Add 4 **Button**'s to your VBoxContainer. Name them "Home", "Quit Game", "Restart", and "Resume".
+
+ Each **Button** should have:
+ - Font size 30px.
+ - A "pressed()" signal connected to the `hud.gd` script. Each function name needs to be different.
+5. Go to your `hud.gd` script.
+
+ Add this functionality for each of the buttons to work properly.
+
+ ```gdscript title="hud.gd"
+ extends Control
+
+ # Reference to the pause menu UI (a PanelContainer node in your scene).
+ @onready var pause_menu = $PanelContainer
+
+ # Preloads the main menu scene so it’s ready to load when the player chooses to go home.
+ @onready var home_scene = preload("res://main_menu.tscn") as PackedScene
+
+ # This function runs when the node enters the scene tree for the first time.
+ func _ready() -> void:
+ # Hide the pause menu when the game starts.
+ pause_menu.hide()
+
+ func _process(delta: float) -> void:
+ pass
+
+ func _on_pause_pressed() -> void:
+ # Show the pause menu and pause the game.
+ pause_menu.show()
+ get_tree().paused = true # This stops the game’s logic and animations.
+
+ func _on_home_pressed() -> void:
+ # Unpause the game and hide the pause menu before going to the main menu.
+ get_tree().paused = false
+ pause_menu.hide()
+ get_tree().change_scene_to_file("res://main_menu.tscn") # Switch to the main menu scene.
+
+ func _on_quit_pressed() -> void:
+ # Quit the game application.
+ get_tree().quit()
+
+ func _on_restart_pressed() -> void:
+ # Unpause the game and reload the current level.
+ get_tree().paused = false
+ get_tree().reload_current_scene() # Reloads the currently active scene.
+
+ func _on_resume_pressed() -> void:
+ # Hide the pause menu and unpause the game to continue playing.
+ pause_menu.hide()
+ get_tree().paused = false
+
+ ```
+
+
+
+
+
+## Checklist
+
+- [ ] I've made title screen.
+- [ ] There are 2 or more levels to play.
+- [ ] I've made a levels page.
+- [ ] I can pause, play, and exit a game!
+
+
\ No newline at end of file