An enemy dies and drops something. Most of the time it is a common sword. Now and then it is a rare staff. Once in a long while, a legendary crown. You want exactly that feel, and you have a table of chances: 60, 25, 10, 4, 1. How does one random number turn into one item, with the right odds?
The answer is a picture worth keeping in your head: a number line, cut into slices.
Weights are lengths
A weight is how much room an item takes up on the line. It is not a percentage, and the weights do not have to add up to 100. Weights of 6, 2.5, 1, 0.4 and 0.1 make the same line as 60, 25, 10, 4 and 1, just shorter. The chance of an item is its slice's length divided by the whole line.
That one idea answers most loot table questions:
- Make something twice as likely? Double its weight. Its slice doubles in length.
- Add a new item? Add a slice. Every other item's chance shrinks a little, because the line got longer.
- Turn something off for now? Give it weight 0. A slice with no length can never be landed on. Godot's docs say the same: "a value of 0 means it will never be returned as an index."
Drag the weights below and watch the slices.
A loot table is a number line
Interactive, engine-accurate- Common 60 (60%)
- Uncommon 25 (25%)
- Rare 10 (10%)
- Epic 4 (4%)
- Legendary 1 (1%)
The line is 100 long. Press Draw once: the roll picks a point on it, and the slice under the point wins.
Rarer tiers get shorter slices, so fewer rolls land on them. Doubling a weight doubles the length of its slice, and its share is its length divided by the whole line.
This runs the GDScript from this article, ported to the browser. The same seed gives the same result in Godot 4.7.2.
Following one draw
On seed 7, the first draw makes randf() return about 0.4315. randf() gives a random number between 0 and 1. Times the total of 100, that is a point at 43.15 on the line.
Now walk the slices from the left, subtracting each weight:
- 43.15 minus 60 (Common) is below zero. The point is inside the Common slice. Common.
The second draw lands at 70.71:
- 70.71 minus 60 is 10.71. Still above zero, so it is past Common. Keep going.
- 10.71 minus 25 (Uncommon) is below zero. Uncommon.
The third lands at 90.04: past Common, past Uncommon (5.04 left), and inside Rare. Subtracting until you go below zero is the same as asking "which slice is this point in?", one slice at a time.
A thousand draws later
Press Draw 1,000 before any single draws, on seed 7, and you get 619 Common, 229 Uncommon, 97 Rare, 42 Epic and 13 Legendary. The weights promise 600, 250, 100, 40 and 10. Close, not exact, and that is what random looks like.
Run it again and the numbers change. Over many runs they hover around what the weights promise. Over a few drops they can stray a long way, which is why a player can open twenty chests without a rare item. If streaks feel unfair in your game, weights alone will not fix it. The Godot docs show a different tool for that, the shuffle bag, in their random number generation tutorial.
"Nothing" is a slice too
Not every enemy drops something. Treat "no drop" as one more slice. The 2D roguelike course's loot lessons roll two chances in a row. First, randf() > 0.55 means no drop, so 45% of enemies drop something. Then, of those drops, 60% are gold and the rest health.
Chances in a row multiply, so that is the same as one line with three slices:
| Slice | Weight | How it comes about |
|---|---|---|
| Nothing | 55 | 55% roll no drop |
| Gold | 27 | 45% drop × 60% gold |
| Health | 18 | 45% drop × 40% health |
Writing it as one table makes it easier to read and to tune. Want more health drops without changing how often enemies drop? Move weight from Gold to Health, and Nothing stays 55.
Where weighted drops go wrong
Rolling each item separately. if randf() < 0.1: drop rare for every item is a different system. Each item rolls on its own, so one enemy can drop three items, or none. That suits "each enemy has a 10% chance to also drop a key". It does not suit "drop exactly one item from this table".
Using percentages that do not add up. Weights of 60, 30 and 20 are fine as weights. As percentages they would be 110%. If you write chances as percentages and then add an item, the old percentages quietly become wrong. Weights do not have that problem.
All weights zero. Then there is no line. rand_weighted() returns -1, which is not a valid index. Check for it before you read the item.
A table that never repeats a seed. If your loot rolls use the global randf(), they ignore your level's seed. Use a RandomNumberGenerator you seeded yourself, as the seeds part of the dungeon series explains.
Very large tables. Walking the line is fast for dozens of items. With thousands, keep a list of running totals and binary search it, or build an alias table. Keith Schwarz's Darts, Dice, and Coins explains both.
In Godot: three ways to write it
By hand, so you can see the number line:
func pick_weighted(weights: Array[float], rng: RandomNumberGenerator) -> int: var total := 0.0 for w in weights: total += w var point := rng.randf() * total for i in weights.size(): point -= weights[i] if point < 0.0: return i # Only reached through rounding, when the roll lands on the very end. for i in range(weights.size() - 1, -1, -1): if weights[i] > 0.0: return i return -1It returns an index into your weights, so keep items and weights in the same order. The last loop handles one rare case: rounding can leave the point a hair past the end of the line. In a test of 10,000 draws in Godot 4.7.2, this function picked the same item as the built-in rand_weighted() every time.
With whole numbers, if your table reads like "70 skeletons, 30 monsters". randi_range(0, total - 1) picks a whole point on the line:
func pick_weighted_int(weights: Array[int], rng: RandomNumberGenerator) -> int: var total := 0 for w in weights: total += w var roll := rng.randi_range(0, total - 1) for i in weights.size(): roll -= weights[i] if roll < 0: return i return -1With a Resource, so designers can fill the table in the Inspector. A Resource is Godot's saveable data object. One small resource holds an item and its weight:
class_name LootEntryextends Resource## One line of a loot table: what drops, and how big its slice of the line is.@export var item: Resource@export_range(0.0, 1000.0) var weight := 1.0A second holds the table, and passes the weights to rand_weighted():
class_name LootTableextends Resource## A loot table you can fill in the Inspector.@export var entries: Array[LootEntry] = []func roll(rng: RandomNumberGenerator) -> Resource: var weights := PackedFloat32Array() for entry in entries: weights.append(entry.weight) var index := rng.rand_weighted(weights) if index < 0: return null return entries[index].itemCreate a LootTable as a .tres file, add entries in the Inspector, and give each enemy one. rand_weighted() needs Godot 4.3 or later. On older versions, call pick_weighted() instead.
Build it next
2D Roguelike: Loot & Corruption
A quest on CodingQuests: 8 interactive lessons that build one part of a game in Godot, in your browser, checked as you go.
The 2D Roguelike's Loot & Corruption quest builds the drops and upgrades of a real game. Lessons 1 and 2, both free, make enemies drop health and gold using fixed randf() chances, the "nothing is a slice too" table above. Lesson 4, a member lesson, offers upgrade cards with a plain shuffle.
The quest does not teach weighted tables. Lesson 4 itself mentions that the finished game weights its cards by rarity, as a refinement. Swap in rand_weighted() from this article when you get there.
- Lesson 1: The First PickupFree
- Lesson 2: The Gold CounterFree
- Lesson 4: The UpgradeManagerMembers
Lessons 1 to 3 are free to open, no account needed. Guild Hall, the membership, opens the other 5.
FAQ
How do weighted loot tables work?
Each item gets a weight, and the weights are laid end to end on a line. A random point on that line picks the item whose slice it lands in. An item's chance is its weight divided by the total of all weights.
How do I do weighted random selection in Godot 4?
Put the weights in a PackedFloat32Array and call rng.rand_weighted(weights) on a RandomNumberGenerator. It returns the index of the chosen weight. It is available from Godot 4.3. On older versions, sum the weights, multiply randf() by the sum and subtract weights until the result goes below zero.
Do loot table weights need to add up to 100?
No. Weights are relative. 6, 3 and 1 behave exactly like 60, 30 and 10. Each item's chance is its weight divided by the total, whatever the total is.
Sources and testing
- Godot docs: RandomNumberGenerator.rand_weighted() (absent from the 4.2 docs, present in 4.3), Random number generation (weighted random, shuffle bags)
- Godot source,
core/math/random_pcg.cpp:rand_weighted()multipliesrandf()by the weight sum and subtracts weights until the remainder goes below zero (godotengine/godot on GitHub) - Keith Schwarz, Darts, Dice, and Coins: Sampling from a Discrete Distribution
- Developer questions this answers: Custom resource - export weighted probability for array? and How do I add probability to pick_random() on the Godot Forum, How do I create a weighted collection and then pick a random element from it? on Game Development Stack Exchange
How this was tested. Every GDScript block on this page was run in Godot 4.7.2 (headless, on Windows) on 31 October 2026, including LootTable with real LootEntry resources. The by-hand pick matched rand_weighted() on 10,000 of 10,000 draws. The demo's random numbers come from a port of Godot's generator, checked number for number against Godot, so seed 7's draws here are seed 7's draws in Godot.

