Create your own
Lesson illustration

Processing Game Data with Arrays and For Loops

Hello again. Last lesson introduced conditionals: your script could check health, ammunition, and cooldown state, then choose an appropriate action. That decision-making becomes much more useful once the game has more than one enemy, spawn point, reward, or UI element to manage.

Today you will use arrays to store a collection of related game data and for loops to process each item in that collection. You will build a small combat-roster simulation: several enemies have names, health values, and active states; a training round damages each living enemy; defeated enemies grant exactly one pistol bullet.


Arrays: one variable holding many values

Until now, a variable has held one piece of state:

var enemy_health: int = 60
var pistol_bullets: int = 0

That works for one enemy. But separate variables become unmanageable as soon as an arena contains several enemies:

var enemy_one_health: int = 60
var enemy_two_health: int = 25
var enemy_three_health: int = 80

An array stores an ordered collection of values under one name instead:

var enemy_health: Array[int] = [60, 25, 80]

The values have numbered positions called indices. Array indices begin at 0, not 1.

IndexValue in enemy_health
060
125
280

Use square brackets to access an element:

print(enemy_health[0]) # Prints 60.
print(enemy_health[1]) # Prints 25.

You can also change a value at an index:

enemy_health[1] = 10

For game data with a known element type, prefer a typed array. It makes the intended contents clear and helps Godot identify mistakes:

var enemy_names: Array[String] = ["Rook", "Viper", "Sentry"]
var enemy_health: Array[int] = [60, 25, 80]
var enemy_active: Array[bool] = [true, true, true]

The three arrays above are deliberately aligned: index 0 refers to Rook in every array, index 1 refers to Viper, and so on. This arrangement is called parallel arrays. It is useful for learning how collections work, but it requires care: every related array must remain in the same order and have the same number of entries.

Later, your project will normally work with actual enemy nodes rather than several parallel lists. The central idea remains the same: keep a collection, then process its members systematically.

GDScript reference — Godot Engine (stable) documentation in English

Read the official Godot reference to establish the precise behavior of Arrays, typed arrays, and for loops. It is a useful reference to revisit whenever Godot reports a type or indexing error.

In Container built-in types, read the Array subsection from Array behavior, paying attention to dynamic resizing and zero-based indexing. Continue through Typed arrays, focusing on typed array syntax and why Array[int] cannot accept a string. Then find the for subsection under control flow. Read the loop overview and its code examples. For today, concentrate on iterating through an array and on range(); dictionaries are only a brief reference for later.

Two everyday Array operations are especially useful at this stage:

var defeated_enemies: Array[String] = []

defeated_enemies.append("Viper")
print(defeated_enemies.size()) # Prints 1.
  • append(value) adds one value at the end.
  • size() reports how many elements the array currently holds.

for loops: apply one rule to every item

A for loop repeats a block of code once for each element in a collection. The basic form is:

for item in collection:
	# Use item here.

For example, this prints every enemy name:

var enemy_names: Array[String] = ["Rook", "Viper", "Sentry"]

for enemy_name: String in enemy_names:
	print("Enemy in arena: ", enemy_name)

During the first pass, enemy_name is "Rook". During the second pass, it is "Viper". The loop ends after it processes "Sentry".

This is called direct iteration. It is the clearest approach when you only need the current value.

A loop can also process a sequence of numbers using range():

for enemy_index in range(3):
	print(enemy_index)

This prints:

0
1
2

The end value is excluded. Therefore, range(3) produces the valid indices for an array containing three elements: 0, 1, and 2.

When you need both an array’s position and its value, combine range() with size():

for enemy_index in range(enemy_names.size()):
	print(enemy_index, ": ", enemy_names[enemy_index])

This indexed form is important when related arrays must be read or updated together:

var enemy_names: Array[String] = ["Rook", "Viper"]
var enemy_health: Array[int] = [60, 25]

for enemy_index in range(enemy_names.size()):
	print(enemy_names[enemy_index], " has ", enemy_health[enemy_index], " health.")

Use this practical rule:

SituationPrefer
You only need to read each itemfor item in items:
You need an item’s positionfor index in range(items.size()):
You must update values stored in an arrayAn indexed loop

One subtle mistake is worth avoiding:

for health in enemy_health:
	health -= 20

This does not update the values inside enemy_health. Here, health is only the current loop variable. To write back into an integer array, address the actual array element by index:

for enemy_index in range(enemy_health.size()):
	enemy_health[enemy_index] -= 20

Watch: loops as a game-development tool

The following short sections reinforce the syntax before you build the combat roster. The final section also gives a useful preview of how loops can later create multiple enemies or other repeated game objects.

For Loops in Godot- The Non-Coder's Guide to GDScript 13

Watch “For Loops in Godot — The Non-Coder's Guide to GDScript 13” by ACB_Gamez for a visual introduction to looping through lists and repeating an action a fixed number of times.

Watch loop basics for the purpose and syntax of a loop, including the scope of the loop variable. Then watch using range for range() and its use in repeated tasks such as spawning several enemies. Keep the distinction clear: a loop over an array processes its contents, while a loop over range() processes numbers.


Build: process a small enemy roster

Open scripts/arena.gd, replacing the previous combat-decision test with the script below. This does not yet create visible enemies in the 3D scene. Instead, it models the underlying data rules first, which lets you verify the logic cleanly in the Output panel.

extends Node3D


var enemy_names: Array[String] = ["Rook", "Viper", "Sentry"]
var enemy_health: Array[int] = [60, 25, 0]
var enemy_active: Array[bool] = [true, true, false]

var defeated_enemies: Array[String] = []
var pistol_bullets: int = 0


func _ready() -> void:
	print("=== Arena roster before training ===")
	print_enemy_roster()

	print("=== Training round: 30 damage to each living enemy ===")
	apply_training_round_damage(30)

	print("=== Arena roster after training ===")
	print_enemy_roster()

	print("Enemies defeated this round: ", defeated_enemies)
	print("Pistol bullets earned: ", pistol_bullets)


func print_enemy_roster() -> void:
	for enemy_index in range(enemy_names.size()):
		var enemy_name: String = enemy_names[enemy_index]
		var health: int = enemy_health[enemy_index]

		if not enemy_active[enemy_index]:
			print(enemy_name, " is already defeated.")
		elif health <= 25:
			print(enemy_name, " is wounded. Health: ", health)
		else:
			print(enemy_name, " is fighting. Health: ", health)


func apply_training_round_damage(damage: int) -> void:
	for enemy_index in range(enemy_names.size()):
		var enemy_name: String = enemy_names[enemy_index]

		if not enemy_active[enemy_index]:
			print(enemy_name, " is skipped because it is defeated.")
			continue

		enemy_health[enemy_index] -= damage

		if enemy_health[enemy_index] <= 0:
			enemy_health[enemy_index] = 0
			enemy_active[enemy_index] = false

			defeated_enemies.append(enemy_name)
			pistol_bullets += 1

			print(enemy_name, " was defeated. One pistol bullet earned.")
		else:
			print(enemy_name, " survived. Health: ", enemy_health[enemy_index])

Save the file and run the scene with F6. The exact array formatting in Output can vary, but the important events should be clear:

  • Rook starts with 60 health, takes 30 damage, and survives.
  • Viper starts with 25 health, takes 30 damage, reaches 0, and is defeated.
  • Sentry is already inactive, so the loop skips it.
  • Viper’s name is appended to defeated_enemies.
  • The player gains exactly one pistol bullet for that defeat.

Read the loop carefully

Focus on this part:

for enemy_index in range(enemy_names.size()):

Because enemy_names has three elements, the loop runs using indices 0, 1, and 2. On each pass, that same index locates the corresponding name, health, and active value:

var enemy_name: String = enemy_names[enemy_index]
enemy_health[enemy_index] -= damage

That is why the arrays must remain aligned. If "Viper" were moved to a different position in enemy_names without also moving its health and active value, the data would no longer describe the right enemy.

continue: skip the rest of one pass

This conditional protects defeated enemies from receiving another reward:

if not enemy_active[enemy_index]:
	print(enemy_name, " is skipped because it is defeated.")
	continue

continue skips all remaining lines in the current loop pass and begins the next one. Thus, Sentry is reported as skipped, but it does not lose health, get appended to defeated_enemies, or award another bullet.

This is the same kind of defensive logic you used with return in the previous lesson, but at loop level:

  • return ends an entire function.
  • continue ends only the current pass through a loop.

The reward rule is intentionally located inside the defeat condition:

if enemy_health[enemy_index] <= 0:
	# Mark defeated.
	# Record the enemy.
	# Award one bullet.

That placement means a bullet is awarded only when a living enemy becomes defeated during the current processing pass.


Inspecting and changing the data

To confirm that you understand the collection rather than merely reproducing the code, make a few controlled edits, run again, and observe the Output.

Try these one at a time:

  1. Change Rook’s starting health from 60 to 30. Rook should now be defeated and award one bullet.
  2. Change the damage passed to apply_training_round_damage(30) to 10. Viper should survive with 15 health and no bullet should be earned.
  3. Add a fourth enemy consistently across all three parallel arrays:
var enemy_names: Array[String] = ["Rook", "Viper", "Sentry", "Scout"]
var enemy_health: Array[int] = [60, 25, 0, 45]
var enemy_active: Array[bool] = [true, true, false, true]

Notice that the loops require no changes. Since they use enemy_names.size(), they automatically process the new fourth entry.

If you accidentally add an enemy name but forget its health or active value, Godot will eventually report an Invalid access to property or key error when the loop reaches an index missing from another array. That is a useful clue: inspect the lengths and ordering of your related arrays.


Key takeaways

An Array stores multiple ordered values in one variable. Its indices begin at 0, and typed arrays make the intended kind of data explicit:

var enemy_health: Array[int] = [60, 25, 80]

A for loop lets one rule process every item in a collection. Use direct iteration when you only need values:

for enemy_name in enemy_names:
	print(enemy_name)

Use an indexed loop when you need to read or update matching values across arrays:

for enemy_index in range(enemy_names.size()):
	enemy_health[enemy_index] -= 20

Finally, continue lets a loop skip invalid or irrelevant items, such as enemies already defeated. That safeguard is essential for the eventual kill-to-bullet loop: each enemy must be able to grant its one bullet exactly once.

This completes the GDScript foundations module. Next, you will begin organizing Godot projects with reusable scenes and learn how to choose node types according to their responsibilities.

Can't find a good explanation? Sign up and we'll make it for you

Sign up