Random Order Creation

Godot Version

4.6.2

Question

Disclaimer that I’ve picked up game dev as my “summer hobby” this year, and only really started in the last week-ish, so bear with me if my technical lingo / explanation of things isn’t super great.

I am working on a collectathon/roguelike farming game and am currently trying to come up with a way of creating random “orders” that the player has to fulfill in order to get points to meet a quota.

I’d like these orders to have a sampling of multiple items and amounts based on how far the player has gotten in the game.

Example:

1 quota completed might have an order for 15 apples and 16 carrots, while after you’ve completed 5 quotas and upgraded your farm with your money, you might have to collect 30 apples, 12 oranges, 32 carrots and 6 gemstones or something.

So far, I’ve been able to stumble my way through tutorials for most things I want to do, but I can’t seem to figure out how to even search this up in a way that is going to give me relevant results. I am hoping that someone out there can point me in the right direction of what to look into, or explain how they would approach the issue.

It depends on the complexity and scale of the game you are making. Do you intend for the player to be able to save the game, then continue the next day? How many types of fruits are there and do they have special behaviors and interactions other than being items for quotas?

If this is the first time you’re doing coding, i would suggest keeping the scope and complexity of the game to be simpler. If you haven’t already known this, you can make ‘classes’ in godot. These are just logical arrangements of instructions and data that you can create copies of. But if you don’t always need to make copies of them, you can often just use them to store data (both within the game session and between sessions but between sessions often require more work and usually require it to be a resource). A script is considered a ‘class’ when you put the following line at the top (i usually put it right at the top).

class_name MyClass

Now whenever you are in any script, if you type MyClass, you are referring to this class. And you can use it to store things. But you can only access static variables by default if you don’t instance (sort of create an actual copy) the class. You just need to add the static in front of var.

class_name GameVariables

static var progress

In your script, you then just go GameVariables.progress += 1 whenever the player complete some task. Or if you don’t like the name GameVariables, you can choose any name you want and put it after class_name.

Another alternative is to use autoload singletons. You do this by going to project settings, going to global and adding the relevant script and name you want. This will also allow the info to be easily accessed.

The actual data structures for the order will depend on how much complexity you can tolerate upfront and how much you need. But for most cases, if you haven’t already done it, you should look more into how to use class_names. It’s very useful for projects with a scope like yours.

1 Like

You can start by breaking this down into smaller problems:

  1. Figure out what resources the player has access to
  2. For each resource generate a number of it that’s needed

It’s not apparent how new resources are unlocked from your explanation, but for figuring out how many of each product you need you could try something like

var exact_number := (current_level - level_item_unlocked_at + 1) * increase_per_level
var max_deviation := floori(exact_number * 0.1) # +/- 10%
var product_needed := exact_number + randi_range(-max_deviation, max_deviation)

Pseudocode, but you should get the basic idea. See Random number generation — Godot Engine (stable) documentation in English for general methods for random number generation.

Thank you! This feels like a good starting point. I didn’t want to get too into the details since I’m not super sure of them myself just yet- currently just trying to get each system I want into somewhat working order mostly for the experience.

No problem. I’ve another suggestion. For new coders and for anyone really, if the game is bigger than pong or flappy bird in terms of complexity meaning you have a lot of information or design, you probably want to make a simple game design document. It doesn’t need to be that formal. Like you said the order scales with the player’s progress. You can write down exactly how it scales.

The document can just be in simple words, you might not even need the computer if you prefer writing in real life. When you are coding, you generally want to be solving coding problems, not thinking also at the same time about your game balance. Ideally when you sit down to code, a good chunk of the game should be crystal clear already. Just a couple of pages of your game details can help you a lot.

What you’ve encountered is the problem of difficulty scalability. It’s one of the key problems for every game and falls under wider umbrella of gameplay balancing.

Since balancing is a never-ending task, you want a system that has several “knobs” that let you tune the parameters, and thus - balance the system.

I’d suggest starting by making a prototype that’s completely separated from the game and just generates and finishes orders as you press a key. Once the system works well by itself - incorporate it into the game.
Make a scene with a single node with a script attached and do everything there.

You need 3 main things:

  1. a way to track player progress
  2. a way to specify an order
  3. a way to configure an order, and do so using player progress as a parameter

Let’s track the player progress first in the simplest possible way that was already suggested by someone above: Progress is just a number that gets incremented by 1 whenever an order is completed. The completion is simulated by pressing the space key:

extends Control

var progress: int = 0

func _input(event):
	if event.is_action_pressed("ui_accept"):
		complete_current_order()
		
func complete_current_order():
	progress += 1
	print("ORDER COMPLETED (%d)"%progress)

The output in the console as we press the space key is now:

ORDER COMPLETED (1)
ORDER COMPLETED (2)
ORDER COMPLETED (3)
...

Next, introduce a way to specify orders. I’ll simply store the order as a dictionary where the key is the item name and value is the required number of those items. Like this: {"apples": 10, "oranges": 5}
Extending our code with the order creation function and creating a new order whenever the current order is completed:

extends Control

var progress: int = 0
var current_order: Dictionary

func _ready():
	current_order = create_order()

func _input(event):
	if event.is_action_pressed("ui_accept"):
		complete_current_order()
		current_order = create_order()
		
func complete_current_order():
	progress += 1
	print("ORDER COMPLETED (%d)"%progress)
	
func create_order() -> Dictionary:
	var order = {"apples": 10, "orranges": 5}	
	print("NEW ORDER: ", order)
	return order

Our output is now:

NEW ORDER: { "apples": 10, "orranges": 5 }
ORDER COMPLETED (1)
NEW ORDER: { "apples": 10, "orranges": 5 }
ORDER COMPLETED (2)
NEW ORDER: { "apples": 10, "orranges": 5 }

We are now capable of tracking player progress and creating new dummy orders.
Next, we will create actual orders by parametrizing our order creation function. As input parameters it’ll take the ingredient count and a list of possible ingredients:

extends Control

var progress: int = 0
var current_order: Dictionary
const INGREDIENTS := ["apples", "oranges", "carrots", "bananas", "pears", "gems"]


func _ready():
	current_order = create_order(INGREDIENTS, 3)


func _input(event):
	if event.is_action_pressed("ui_accept"):
		complete_current_order()
		current_order = create_order(INGREDIENTS, 3)
		
		
func complete_current_order():
	progress += 1
	print("ORDER COMPLETED (%d)"%progress)
	
	
func create_order(ingredient_pool: Array, ingredient_count = 3) -> Dictionary:
	var order = {}
	for i in ingredient_count:
		order[ingredient_pool.pick_random()] = randi_range(5, 10)
	print("NEW ORDER: ", order)
	return order

Our output is now:

NEW ORDER: { "carrots": 8, "gems": 6 }
ORDER COMPLETED (1)
NEW ORDER: { "carrots": 6, "bananas": 7, "apples": 7 }
ORDER COMPLETED (2)
NEW ORDER: { "gems": 5, "pears": 5, "oranges": 8 }
ORDER COMPLETED (3)
NEW ORDER: { "pears": 9, "gems": 7, "bananas": 5 }

Now for the most interesting and important part. We want to drive the order creation parameters with the player progress. We’ll start simply by increasing the number of item types in an order as the player progresses.
A neat tool for that are Curve resources. They let you specify one value depending on another in a visual way. So we’ll add an exported Curve property whose x axis will represent the player progress and y axis the number of item types in the order. In the inspector we can create this curve resource and adjust it. In the code we’ll sample the curve with the player progress as the x value and pass the returned sample into the order creation function.

extends Control

@export var item_types_per_order: Curve
var progress: int = 0
var current_order: Dictionary
const INGREDIENTS := ["apples", "oranges", "carrots", "bananas", "pears", "gems"]


func _ready():
	current_order = create_order(INGREDIENTS, round(item_types_per_order.sample(progress)))


func _input(event):
	if event.is_action_pressed("ui_accept"):
		complete_current_order()
		current_order = create_order(INGREDIENTS, round(item_types_per_order.sample(progress)))
		
		
func complete_current_order():
	progress += 1
	print("ORDER COMPLETED (%d)"%progress)
	
	
func create_order(ingredient_pool: Array, ingredient_count = 3) -> Dictionary:
	ingredient_pool = ingredient_pool.duplicate()
	var order = {}
	for i in ingredient_count:
		var item = ingredient_pool.pick_random()
		ingredient_pool.erase(item)
		order[item] = randi_range(5, 10)
	print("NEW ORDER: ", order)
	return order

All of a sudden, we’re getting quite powerful. We drive the item type count with the player progress and have the ability to tune/balance the relation between the progress and the count by merely tweaking a curve. Our output now looks like this:

NEW ORDER: { "oranges": 6 }
ORDER COMPLETED (1)
NEW ORDER: { "gems": 9 }
ORDER COMPLETED (2)
NEW ORDER: { "oranges": 8 }
ORDER COMPLETED (3)
NEW ORDER: { "apples": 9, "pears": 5 }
ORDER COMPLETED (4)
NEW ORDER: { "apples": 9, "oranges": 7 }
ORDER COMPLETED (5)
NEW ORDER: { "apples": 6, "oranges": 7, "bananas": 7 }
ORDER COMPLETED (6)
NEW ORDER: { "apples": 5, "oranges": 7, "carrots": 5 }
ORDER COMPLETED (7)
NEW ORDER: { "carrots": 7, "oranges": 7, "bananas": 10, "pears": 6 }
ORDER COMPLETED (8)
NEW ORDER: { "oranges": 10, "bananas": 10, "gems": 10, "apples": 8 }
ORDER COMPLETED (9)
NEW ORDER: { "carrots": 5, "pears": 7, "oranges": 9, "apples": 5, "bananas": 10 }

But why stop here. We can drive more parameters with more curves. Let’s add a curve that controls the average amount per item so that too can increase with the player progress:

extends Control

@export var item_types_per_order: Curve
@export var average_item_amount: Curve
var progress: int = 0
var current_order: Dictionary
const INGREDIENTS := ["apples", "oranges", "carrots", "bananas", "pears", "gems"]


func _ready():
	current_order = create_order()


func _input(event):
	if event.is_action_pressed("ui_accept"):
		complete_current_order()
		current_order = create_order()
		
		
func complete_current_order():
	progress += 1
	print("ORDER COMPLETED (%d)"%progress)
	
	
func create_order() -> Dictionary:
	var ingredient_count: int = round(item_types_per_order.sample(progress))
	var ingredient_pool: Array = INGREDIENTS.duplicate()
	var amount: int = round(average_item_amount.sample(progress))
	var order = {}
	for i in ingredient_count:
		var item = ingredient_pool.pick_random()
		ingredient_pool.erase(item)
		order[item] = randi_range(amount * 0.8, amount * 1.2)
	print("NEW ORDER: ", order)
	return order

And our output now looks like this:

NEW ORDER: { "pears": 1 }
ORDER COMPLETED (1)
NEW ORDER: { "bananas": 5 }
ORDER COMPLETED (2)
NEW ORDER: { "apples": 10 }
ORDER COMPLETED (3)
NEW ORDER: { "gems": 17, "carrots": 17 }
ORDER COMPLETED (4)
NEW ORDER: { "apples": 17, "pears": 25 }
ORDER COMPLETED (5)
NEW ORDER: { "carrots": 23, "apples": 25, "gems": 31 }
ORDER COMPLETED (6)
NEW ORDER: { "apples": 37, "bananas": 24, "oranges": 36 }
ORDER COMPLETED (7)
NEW ORDER: { "bananas": 28, "oranges": 29, "carrots": 41, "gems": 38 }
ORDER COMPLETED (8)
NEW ORDER: { "gems": 37, "bananas": 43, "pears": 42, "carrots": 36 }
ORDER COMPLETED (9)
NEW ORDER: { "carrots": 36, "oranges": 34, "pears": 41, "gems": 42, "bananas": 34 }
ORDER COMPLETED (10)
NEW ORDER: { "apples": 47, "bananas": 34, "oranges": 43, "pears": 39, "carrots": 41 }
ORDER COMPLETED (11)
NEW ORDER: { "gems": 46, "apples": 46, "pears": 38, "bananas": 47, "carrots": 42 }
5 Likes

Holy moly cannoli! @normalized is posting like me!

I actually had this page saved to come back and give an answer like that today when I had time, but @normalized killed it!

Do that.

EDIT: Also, OMG that curve thing is so cool!

2 Likes

Stuff like this ^^ is what makes this Forum so special. Now that I see it so clearly demonstrated, I can think of a ton of uses for the notion, and will be shamelessly ‘inspiring’ some future gameplay on such a system. Hats off, and a low bow for the lesson. Kudos. Have a great day.

2 Likes

Thank you so much for this! Trying to wrap my head around some of this stuff was a bit of a struggle so I really appreciate how much thought and detail you put into this answer. Saving this to my Game Dev notebook so I can reference it.

4 Likes