Skip to content

feat: 3D infinite racing with procedural generation #471

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ export default defineConfig({
link: "game-design/godot/3dracinggame/0-main-scene/"
}],
collapsed: true
},
{
label: "Procedural Generation", items: [{
label: "Infinite 3D Racing",
link: "game-design/proceduralgeneration/infinite3dtutorial/0-setup/"
},
],
collapsed: true
}],
},
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ This is a game. What you have made is a game. It might not look like it, but if

If you made some 3D models of platforms, a button, a character, and added a skybox it would look like a real game. That's truly the only difference. Well, that, and maybe a goal.

Next you should go join a [game jam](https://itch.io/jams) or go through our [3D racing game](/src/content/docs/game-design/godot/3dracinggame/0-main-scene/) tutorial!
Next you should go join a [game jam](https://itch.io/jams) or go through our [3D racing game](/game-design/godot/3dracinggame/0-main-scene/) tutorial!
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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.
Read through and implement [3D Intro](/game-design/godot/3d-intro/0-making-project/) until you know the basics of how 3D works in Godot.
:::
![Preview of end result](/src/assets/godot/3DRacing/endResultPreview.png)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ To drive the car, you'll need to map these four inputs:
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).
If you don't know how to do this, refer to [this 3D game resource](/game-design/godot/3d-intro/4-changing-controls/).
:::
## Adding a Script

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ 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:
Here's some ideas for what your next steps should be:

<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!
4. Make an endless track through procedural generation! [Link to the tutorial here](/game-design/proceduralgeneration/infinite3dtutorial/0-setup/)
</Steps>

Thanks for following along this far! Happy Godoting and have fun.
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/game-design/godot/basics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ This is used to store version control information in the project folder. It is r
The next window that'll open is the meat of the game engine, it is where you make your actual game. At first, it can be a little daunting, all the buttons and docks, but it's simple to understand once you've used it a bit.
![Empty Godot project](/src/assets/godot/basics/EmptyEngine.png)
For this guide, we will use 2D to introduce all the key concepts.
But, if you're already familiar with game development and want to start with 3D, we have a [3d guide](/game-design/godot/3d) as well. If you choose this you may miss out on core aspects like signals.
But, if you're already familiar with game development and want to start with 3D, we have a [3d guide](/game-design/godot/3d-intro/0-making-project/) as well. If you choose this you may miss out on core aspects like signals.

### The interface

Expand Down Expand Up @@ -364,7 +364,7 @@ For a good introduction to writing your own scripts to control nodes, see the [G

If you have followed this entire guide, you know enough terminology and technique to make almost any 2D game!
You will definitely need to look things up and reference the documentation, no one stops using google, but the more you create, the more you will develop muscle memory and gain understanding.
3D games aren't much more difficult, [read the guide on making a 3D game](/game-design/godot/3d).
3D games aren't much more difficult, [read the guide on making a 3D game](/game-design/godot/3d-intro/0-making-project/).

To test your skills, try adding some simple things almost all 2D platformers will have:
1. Giving the player a double-jump.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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.
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-intro/0-making-project/) 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
type: tutorial
unitTitle: Infinite 3D Procedural Generation
title: Catching up
description: This page works through how to integrate procedural generation with a 3D game
---

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

:::note[If you're a beginner...]
Read through and implement [3D Intro](/game-design/godot/3d-intro/0-making-project/) until you know the basics of how 3D works in Godot.
:::

:::note[Another Note]
If you have already done the [3D Racing Game](/game-design/godot/3dracinggame/0-main-scene/), you can skip this page!
:::

## What you'll be making

![Screenshot preview of the game](/src/assets/proceduralgeneration/3dinfinitegame/finalProductRunning.png)

In this tutorial, you'll work step by step through creating your very own infinite 3D racing game! In this game, the car will navigate across windy roads, straight roads, and bumpy roads infinitely!

You'll learn to:
- Create a car (through a previous tutorial)
- Create a track (also through a previous tutorial)
- Then, a neverending track
- Code randomness into track making
- And code procedural noise into your track!

<details>
<summary>What is procedural noise?</summary>
**Procedural noise** is a way to create natural-looking patterns for things like textures, animations, and 3D models in computer graphics.

It uses math to make patterns that look random but can be repeated exactly when needed. These patterns are smooth, work well in any
direction, and can be made at different levels of detail using powerful computers.

Two common types of procedural noise are **Perlin noise** and **Simplex noise**, both invented by Ken Perlin.

- Perlin noise, made in 1983, creates smooth patterns using a grid but can sometimes show unwanted lines in complex designs.
- Simplex noise, created later in 2001, fixes these problems by using triangles instead of squares, making it faster and
better for more detailed or high-dimensional projects.

Both are great for creating things like clouds, landscapes, and other realistic effects in games and movies.
</details>

## Catching up

### Creating a car

As we are following straight from another tutorial, you will need to create a car. If you want to go through the whole tutorial in detail, [go through this 3D racing tutorial](/game-design/godot/3dracinggame/0-main-scene/).


If you want to quickly make a car, go through:
<Steps>
1. [Making The Car section](/game-design/godot/3dracinggame/1-making-the-car/)
2. [Car Controls first page](/game-design/godot/3dracinggame/2-car-controls/)
3. The second page of "Car Controls": [Driving the Car](/game-design/godot/3dracinggame/2-car-controls/1)
</Steps>
Then, you've made your car!


### Creating a track

This tutorial also assumes you have made at least one track from the 3D racing game tutorial.
To make your track, follow [these steps to catch up.](/game-design/godot/3dracinggame/3-track-building/)
You do not need to implement the last page (the Extra: Boost page) if you don't want to.


<Box>
## Checklist
<Checklist>
- [ ] I have gone through the other tutorials.
- [ ] I know what procedural noise is.
</Checklist>
</Box>
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
---
type: tutorial
title: Using Random Functions
description: This page works through how to integrate procedural generation with a 3D game
---

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

## The Script
Add a GDScript to your **Node3D** EndlessRacing.

:::note[Please Read This Godot Documentation]
Reading through these nodes is ***highly*** recommended.

- [Curve3D](https://docs.godotengine.org/en/stable/classes/class_curve3d.html) (to understand how to randomise the paths' curves),

- [Vector3](https://docs.godotengine.org/en/4.3/classes/class_vector3.html) (the class to make random point positions for the track),

- [Path3D](https://docs.godotengine.org/en/stable/classes/class_path3d.html) (the reason for the Curve3D).
:::

### The **Time Label** from the 3D Racing Tutorial

<Tabs>
<TabItem label="I have the track_1.gd script">
If you have added [the timer from the 3D racing tutorial](/game-design/godot/3dracinggame/3-track-building/2),
move this logic (from `track_1.gd`) into the `endless_racing.gd` script.

This is because only the timer from the old track script will be used in this game.
</TabItem>
<TabItem label="I don't have a previous track script">
<Steps>
1. To make a race car timer, we will need milliseconds, seconds, and minutes.
2. *In the `hud.tscn` scene (which is also located in the `car.tscn` scene tree):* 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="endless_racing.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!
</Steps>
</TabItem>
</Tabs>

### Explanation of How To Add a Random Path3D Points

To add another segment onto the track, we will be adding a new point into the Curve3D's list of points.
For reference, these will be the points we will be deleting and adding onto.

Notice the "In" and "Out" **Vector3**'s.
![picture of Curve3D's points](/src/assets/proceduralgeneration/3dinfinitegame/curvesPointsExample.png)


Here is another photo explaining what we will be doing via code:
![picture of randomly adding a Curve3D point explanation](/src/assets/proceduralgeneration/3dinfinitegame/explanationOfAddingANewPoint.png)

### Making a Track Using Randomness

This will be implementing the endless racing concept in code.


*Add this code onto your existing code.* Feel free to delete the comments after you've understood the code.

```gdscript title="endless_racing.gd"
extends Node3D

# Reference to the car object
@onready var car = $VehicleBody3D as VehicleBody3D
var path_curve: Curve3D # The curve that defines the track
# Tracks if the car has passed halfway through the current road segment
var passed_road_segment: bool = false

func _ready() -> void:
# Get the curve from the Path3D node at the start of the game
var starting_path = $Path3D as Path3D
# Store the Curve3D object for later modifications
path_curve = starting_path.get_curve()

func _process(delta: float) -> void:
# Ensure there are points in the curve before processing (this should always be true)
if path_curve.get_point_count() == 0:
return

# Get the position of the second point in the curve
# Because we remove the first point in the function below,
# this second point in the path curves' list will always change to the next point.
var second_point: Vector3 = path_curve.get_point_position(1)
# Get the current global position of the car
var car_pos: Vector3 = car.global_position

# Check if the car is near the second point of the track
if second_point.distance_squared_to(car_pos) < 100.0:
# Car has passed the segment, set the flag
passed_road_segment = true
# Check if the car has moved far past the segment and if the flag is set
# Because when the segment is removed the car won't be falling, but driving on the newer segment
elif second_point.distance_squared_to(car_pos) > 1000.0 && passed_road_segment == true:
# Call the function to add a new road segment while removing the old one
# Because we are only using random functions, these track segments will be more varied and curvy.
remove_and_add_curvier_point()
# Reset the flag for the next segment
passed_road_segment = false

func remove_and_add_curvier_point():
# Remove the first point in the curve to free up memory
# This will also change the second_point's point above.
if path_curve.get_point_count() > 0:
path_curve.remove_point(0)

# Get the last point's position in the curve
var last_index = path_curve.get_point_count() - 1
var last_position: Vector3 = path_curve.get_point_position(last_index)

# Define the offset for the new position:
# Forward movement along the Z-axis, with minor random offsets for variety
var forward_offset = Vector3(0, 0, randf_range(100, 200))
var lateral_offset = Vector3(randf_range(-30, 30), randf_range(-5, 5), 0)
var new_position = last_position + forward_offset + lateral_offset

# Calculate control tangents for a smooth curve
var control_length = forward_offset.length() * 0.8 # Length of control handles
var direction = (new_position - last_position).normalized() # Unit vector of movement direction

# Define the "out" curve (control handle) for the last point
var out_curve = direction * control_length
out_curve.x += randf_range(-50, 50) # Add randomness to X for sharp lateral turns
out_curve.y += randf_range(-10, 10) # Optional: slight vertical variation
out_curve.z *= randf_range(0.8, 1.2) # Vary forward movement length slightly
path_curve.set_point_out(last_index, out_curve) # Set the "out" control for the last point

# Define the "in" curve (control handle) for the new point
var in_curve = -out_curve # Ensure smooth transitions by mirroring the "out" curve

# Add the new point to the curve with its control handles
path_curve.add_point(new_position, in_curve, out_curve)

# Update the Path3D node to reflect the updated curve
$Path3D.set_curve(path_curve)
```

### Test it out!

Run your game and your track and see what your code makes!

Change these variables if you want a:
<Steps>
1. Curvier or straighter track:
Consider changing the "In" and "Out" curves to smaller random ranges (for a straighter track), or larger random ranges (for a curvier track).
2. Longer or shorter track:
Consider changing the `forward_offset` to smaller random ranges (for a shorter track), or larger random ranges (for a longer track).
3. More bumps/hills in the road:
Consider changing the `lateral_offset` to smaller random ranges (for a smooth track), or larger random ranges (for a bumpier/hilly track).
</Steps>
<Box>
## Checklist
<Checklist>
- [ ] I have made/put the timer logic into my script, and when I run it, the timer starts.
- [ ] I put the endless racing concept in code and understand how it works.
- [ ] I have implemented randomness into dynamic track making and understand how I can change different numbers and elements of the track to get my desired outcome.
- [ ] My track is driveable (inputting some crazy numbers will make your track too hard to drive on).
</Checklist>
</Box>
Loading
Loading