Space invaders for everyone

  • Create a new Node2D
  • Attach a script.
  • Copy content below
  • and start playing!..

Just sharing for fun and maybe you can extend on it!.

extends Node2D

# Game Variables
var screen_size
var player_speed = 300
var game_over = false
var game_started = false
var score = 0
var lives = 3
var level = 1
var enemy_shot_chance = 0.0005  # Reduced from 0.002 - chance per enemy per frame to shoot

# Mothership variables
var mothership = null
var mothership_active = false
var mothership_timer = 0
var mothership_interval = 15.0  # Seconds between mothership appearances
var mothership_speed = 100
var mothership_direction = 1

# Nodes
var player
var player_projectiles = []
var enemies = []
var enemy_projectiles = []
var bunkers = []
var score_label
var lives_label
var message_label
var commentary_label

# Constants
const PLAYER_SIZE = Vector2(40, 20)
const ENEMY_SIZE = Vector2(30, 30)
const MOTHERSHIP_SIZE = Vector2(60, 25)
const PROJECTILE_SIZE = Vector2(5, 15)
const BUNKER_BLOCK_SIZE = Vector2(10, 10)
const ENEMY_ROWS = 4
const ENEMY_COLS = 8
const ENEMY_SPACING = Vector2(50, 50)
const ENEMY_MOVE_SPEED = 20
const ENEMY_DROP_AMOUNT = 10
const MAX_PLAYER_PROJECTILES = 3
const PROJECTILE_SPEED = 400

# Enemy movement
var enemy_direction = 1  # 1 for right, -1 for left
var enemy_move_timer = 0
var enemy_move_interval = 0.5  # Time between enemy movements


func _ready():
	randomize()
	screen_size = get_viewport_rect().size
	
	# Create player
	player = ColorRect.new()
	player.color = Color.GREEN
	player.size = PLAYER_SIZE
	player.position = Vector2(screen_size.x / 2 - player.size.x / 2, screen_size.y - player.size.y - 20)
	add_child(player)
	
	# Create score and lives labels
	score_label = Label.new()
	score_label.position = Vector2(20, 20)
	score_label.text = "Score: 0"
	add_child(score_label)
	
	lives_label = Label.new()
	lives_label.position = Vector2(screen_size.x - 100, 20)
	lives_label.text = "Lives: 3"
	add_child(lives_label)
	
	# Create message label (center of screen)
	message_label = Label.new()
	message_label.position = Vector2(screen_size.x / 2 - 100, screen_size.y / 2 - 20)
	message_label.text = "Press SPACE to start"
	add_child(message_label)
	
	# Create commentary label (bottom of screen)
	commentary_label = Label.new()
	commentary_label.position = Vector2(screen_size.x / 2 - 150, screen_size.y - 40)
	commentary_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	commentary_label.size = Vector2(300, 30)
	commentary_label.visible = false
	add_child(commentary_label)

func _process(delta):
	if game_over:
		if Input.is_action_just_pressed("ui_select"):  # Restart game
			reset_game()
		return
	
	if not game_started:
		if Input.is_action_just_pressed("ui_select"):
			game_started = true
			message_label.visible = false
			setup_level(level)
			show_commentary("Space battle begins! Blast 'em!")
		return
	
	# Player movement
	var player_movement = 0
	if Input.is_action_pressed("ui_left"):
		player_movement = -1
	if Input.is_action_pressed("ui_right"):
		player_movement = 1
	
	player.position.x += player_movement * player_speed * delta
	player.position.x = clamp(player.position.x, 0, screen_size.x - player.size.x)
	
	# Player shooting
	if Input.is_action_just_pressed("ui_select") and len(player_projectiles) < MAX_PLAYER_PROJECTILES:
		create_player_projectile()
	
	# Mothership handling
	process_mothership(delta)
	
	# Update player projectiles
	process_player_projectiles(delta)
	
	# Update enemy movement
	process_enemy_movement(delta)
	
	# Random enemy shooting
	for enemy in enemies:
		if randf() < enemy_shot_chance:
			create_enemy_projectile(enemy)
	
	# Update enemy projectiles
	process_enemy_projectiles(delta)

func process_mothership(delta):
	# Mothership timer and creation
	if not mothership_active:
		mothership_timer += delta
		if mothership_timer >= mothership_interval:
			mothership_timer = 0
			create_mothership()
	else:
		# Move mothership
		mothership.position.x += mothership_speed * mothership_direction * delta
		
		# Remove mothership if off screen
		if (mothership_direction > 0 and mothership.position.x > screen_size.x) or \
		   (mothership_direction < 0 and mothership.position.x < -MOTHERSHIP_SIZE.x):
			remove_child(mothership)
			mothership = null
			mothership_active = false

func process_player_projectiles(delta):
	for i in range(player_projectiles.size() - 1, -1, -1):
		var projectile = player_projectiles[i]
		projectile.position.y -= PROJECTILE_SPEED * delta
		
		# Remove projectile if it goes off screen
		if projectile.position.y < -PROJECTILE_SIZE.y:
			remove_child(projectile)
			player_projectiles.remove_at(i)
			continue
		
		# Check for collision with mothership
		if mothership_active and check_collision(projectile, mothership):
			# Mothership hit
			score += 100 * level  # More points for higher levels
			score_label.text = "Score: " + str(score)
			show_commentary("MOTHERSHIP DOWN! " + str(100 * level) + " POINTS!")
			
			# Remove mothership
			remove_child(mothership)
			mothership = null
			mothership_active = false
			
			# Remove projectile
			remove_child(projectile)
			player_projectiles.remove_at(i)
			continue
		
		# Check for collision with enemies
		var enemy_hit = false
		for j in range(enemies.size() - 1, -1, -1):
			var enemy = enemies[j]
			if check_collision(projectile, enemy):
				# Enemy hit
				score += 10
				score_label.text = "Score: " + str(score)
				
				# Show random hit commentary
				show_hit_commentary()
				
				# Remove enemy
				remove_child(enemy)
				enemies.remove_at(j)
				
				# Remove projectile
				remove_child(projectile)
				player_projectiles.remove_at(i)
				
				enemy_hit = true
				break
		
		if enemy_hit:
			continue
		
		# Check for collision with bunkers
		for j in range(bunkers.size() - 1, -1, -1):
			var bunker_block = bunkers[j]
			if check_collision(projectile, bunker_block):
				# Bunker block hit
				remove_child(bunker_block)
				bunkers.remove_at(j)
				
				# Remove projectile
				remove_child(projectile)
				player_projectiles.remove_at(i)
				break

func process_enemy_movement(delta):
	enemy_move_timer += delta
	if enemy_move_timer >= enemy_move_interval:
		enemy_move_timer = 0
		
		var should_change_direction = false
		var lowest_enemy_y = 0
		
		# Move all enemies
		for enemy in enemies:
			enemy.position.x += ENEMY_MOVE_SPEED * enemy_direction
			
			# Check if any enemy touches the sides
			if enemy.position.x <= 0 or enemy.position.x + enemy.size.x >= screen_size.x:
				should_change_direction = true
			
			# Track lowest enemy
			lowest_enemy_y = max(lowest_enemy_y, enemy.position.y + enemy.size.y)
		
		# Change direction and drop enemies if needed
		if should_change_direction:
			enemy_direction *= -1
			for enemy in enemies:
				enemy.position.y += ENEMY_DROP_AMOUNT
		
		# Check if enemies reached the player
		if lowest_enemy_y >= player.position.y:
			game_over = true
			message_label.text = "Game Over! Press SPACE to restart"
			message_label.visible = true
			show_commentary("They invaded your space! Game over!")
		
		# Check if all enemies are destroyed
		if enemies.size() == 0:
			level += 1
			setup_level(level)
			show_commentary("Level " + str(level) + "! Enemies incoming!")

func process_enemy_projectiles(delta):
	for i in range(enemy_projectiles.size() - 1, -1, -1):
		var projectile = enemy_projectiles[i]
		projectile.position.y += PROJECTILE_SPEED * delta
		
		# Remove projectile if it goes off screen
		if projectile.position.y > screen_size.y:
			remove_child(projectile)
			enemy_projectiles.remove_at(i)
			continue
		
		# Check for collision with player
		if check_collision(projectile, player):
			# Player hit
			lives -= 1
			lives_label.text = "Lives: " + str(lives)
			
			# Show hit commentary
			show_player_hit_commentary()
			
			# Remove projectile
			remove_child(projectile)
			enemy_projectiles.remove_at(i)
			
			if lives <= 0:
				game_over = true
				message_label.text = "Game Over! Press SPACE to restart"
				message_label.visible = true
				show_commentary("You're out of ships! Game over!")
			
			continue
		
		# Check for collision with bunkers
		for j in range(bunkers.size() - 1, -1, -1):
			var bunker_block = bunkers[j]
			if check_collision(projectile, bunker_block):
				# Bunker block hit
				remove_child(bunker_block)
				bunkers.remove_at(j)
				
				# Remove projectile
				remove_child(projectile)
				enemy_projectiles.remove_at(i)
				break

func create_mothership():
	mothership = ColorRect.new()
	mothership.color = Color.PURPLE
	mothership.size = MOTHERSHIP_SIZE
	
	# Randomize direction
	mothership_direction = 1 if randf() > 0.5 else -1
	
	if mothership_direction > 0:
		mothership.position = Vector2(-MOTHERSHIP_SIZE.x, 40)
	else:
		mothership.position = Vector2(screen_size.x, 40)
	
	add_child(mothership)
	mothership_active = true
	show_commentary("MOTHERSHIP INCOMING!")

func create_bunkers():
	# Clear any existing bunkers
	for bunker in bunkers:
		remove_child(bunker)
	bunkers.clear()
	
	# Create 4 bunkers evenly spaced
	var bunker_width = 5 * BUNKER_BLOCK_SIZE.x
	var bunker_height = 3 * BUNKER_BLOCK_SIZE.y
	var spacing = screen_size.x / 5
	
	for b in range(4):
		var bunker_x = spacing * (b + 1) - bunker_width / 2
		var bunker_y = screen_size.y - 150
		
		# Create bunker with a shape like:
		#  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
		# β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
		# β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ
		
		# Top row (5 blocks)
		for i in range(5):
			create_bunker_block(Vector2(bunker_x + i * BUNKER_BLOCK_SIZE.x, bunker_y))
		
		# Middle row (7 blocks)
		for i in range(7):
			var x = bunker_x + (i - 1) * BUNKER_BLOCK_SIZE.x
			create_bunker_block(Vector2(x, bunker_y + BUNKER_BLOCK_SIZE.y))
		
		# Bottom row (7 blocks with gap in middle)
		for i in range(7):
			if i != 3:  # Skip middle block to create doorway
				var x = bunker_x + (i - 1) * BUNKER_BLOCK_SIZE.x
				create_bunker_block(Vector2(x, bunker_y + 2 * BUNKER_BLOCK_SIZE.y))

func create_bunker_block(position):
	var block = ColorRect.new()
	block.color = Color(0, 0.5, 0)  # Dark green
	block.size = BUNKER_BLOCK_SIZE
	block.position = position
	add_child(block)
	bunkers.append(block)

func setup_level(level_num):
	# Clear any existing enemies and projectiles
	for enemy in enemies:
		remove_child(enemy)
	enemies.clear()
	
	for projectile in player_projectiles:
		remove_child(projectile)
	player_projectiles.clear()
	
	for projectile in enemy_projectiles:
		remove_child(projectile)
	enemy_projectiles.clear()
	
	if mothership_active and mothership != null:
		remove_child(mothership)
		mothership = null
		mothership_active = false
	
	# Create bunkers
	create_bunkers()
	
	# Adjust difficulty based on level
	enemy_move_interval = max(0.1, 0.5 - (level_num - 1) * 0.05)
	enemy_shot_chance = min(0.003, 0.0005 + (level_num - 1) * 0.0003)  # Still much lower than original
	
	# Create enemies
	var start_x = (screen_size.x - (ENEMY_COLS * (ENEMY_SIZE.x + ENEMY_SPACING.x))) / 2 + ENEMY_SIZE.x / 2
	var start_y = 70
	
	for row in range(ENEMY_ROWS):
		for col in range(ENEMY_COLS):
			var enemy = ColorRect.new()
			
			# Different colors based on row
			if row == 0:
				enemy.color = Color.RED
			elif row == 1:
				enemy.color = Color.ORANGE
			elif row == 2:
				enemy.color = Color.YELLOW
			else:
				enemy.color = Color(1, 0.5, 0)  # Orange-ish
			
			enemy.size = ENEMY_SIZE
			enemy.position = Vector2(
				start_x + col * (ENEMY_SIZE.x + ENEMY_SPACING.x),
				start_y + row * (ENEMY_SIZE.y + ENEMY_SPACING.y)
			)
			add_child(enemy)
			enemies.append(enemy)
	
	# Reset enemy movement
	enemy_direction = 1
	enemy_move_timer = 0
	
	# Reset mothership timer
	mothership_timer = 0

func create_player_projectile():
	var projectile = ColorRect.new()
	projectile.color = Color.GREEN
	projectile.size = PROJECTILE_SIZE
	projectile.position = Vector2(
		player.position.x + player.size.x / 2 - PROJECTILE_SIZE.x / 2,
		player.position.y - PROJECTILE_SIZE.y
	)
	add_child(projectile)
	player_projectiles.append(projectile)

func create_enemy_projectile(enemy):
	var projectile = ColorRect.new()
	projectile.color = Color.RED
	projectile.size = PROJECTILE_SIZE
	projectile.position = Vector2(
		enemy.position.x + enemy.size.x / 2 - PROJECTILE_SIZE.x / 2,
		enemy.position.y + enemy.size.y
	)
	add_child(projectile)
	enemy_projectiles.append(projectile)

func check_collision(rect1, rect2):
	return (
		rect1.position.x < rect2.position.x + rect2.size.x and
		rect1.position.x + rect1.size.x > rect2.position.x and
		rect1.position.y < rect2.position.y + rect2.size.y and
		rect1.position.y + rect1.size.y > rect2.position.y
	)

func reset_game():
	score = 0
	lives = 3
	level = 1
	score_label.text = "Score: 0"
	lives_label.text = "Lives: 3"
	game_over = false
	message_label.text = "Press SPACE to start"
	message_label.visible = true
	
	# Reset player position
	player.position = Vector2(screen_size.x / 2 - player.size.x / 2, screen_size.y - player.size.y - 20)
	
	# Clear any existing enemies and projectiles
	for enemy in enemies:
		remove_child(enemy)
	enemies.clear()
	
	for projectile in player_projectiles:
		remove_child(projectile)
	player_projectiles.clear()
	
	for projectile in enemy_projectiles:
		remove_child(projectile)
	enemy_projectiles.clear()
	
	if mothership_active and mothership != null:
		remove_child(mothership)
		mothership = null
		mothership_active = false
	
	for bunker in bunkers:
		remove_child(bunker)
	bunkers.clear()
	
	# Reset mothership timer
	mothership_timer = 0
	mothership_active = false

func show_commentary(text):
	commentary_label.text = text
	commentary_label.visible = true
	
	# Auto-hide commentary after a delay
	var timer = get_tree().create_timer(2.0)
	await timer.timeout
	if is_instance_valid(commentary_label):
		commentary_label.visible = false

func show_hit_commentary():
	var comments = [
		"Boom! Got one!",
		"Alien down!",
		"Nice shot!",
		"Target eliminated!",
		"That's a hit!",
		"Blasted!",
		"They're dropping like flies!",
		"One less invader!",
		"Direct hit!",
		"Alien go splat!"
	]
	
	# Add special comments for scoring milestones
	if score % 100 == 0:
		show_commentary("Score: " + str(score) + "! Keep it up!")
		return
	
	# Add special comments for few enemies left
	if enemies.size() <= 3 and enemies.size() > 0:
		comments.append("Almost got 'em all!")
		comments.append("Just " + str(enemies.size()) + " more to go!")
		comments.append("They're almost extinct!")
	
	show_commentary(comments[randi() % comments.size()])

func show_player_hit_commentary():
	var comments = [
		"Ouch! You've been hit!",
		"Shields damaged!",
		"They got you!",
		"Ship hit! " + str(lives) + " left!",
		"Watch out for those lasers!",
		"Hit detected! Keep fighting!",
		"That's gonna leave a mark!",
		"Your ship's on fire!",
		"Engines damaged!",
		"Critical hit!"
	]
	
	# Add special comments based on lives left
	if lives == 1:
		comments = [
			"Last life! Be careful!",
			"One more hit and you're done!",
			"Shields critical!",
			"Final ship remaining!",
			"Don't die now!"
		]
	
	show_commentary(comments[randi() % comments.size()])
17 Likes

That is brilliant! I couldn’t believe it when the mother ships appeared as well! Just fantastic. Even the little encouraging score messages made me smile.

Works perfectly except two small warnings. (Only posting them here for info, no probs, simple fixes if indeed it mattered)
image

I never would have thought of doing the enemies that way. Had to have a good look through the code to get how you had done it. Especially the bunkers. Now I have seen it I would not do it any other way!

That would be a really interesting challenge for a Godot game jam. Produce a complete game in a single script! Too late now though as I think we have a winner!

I am genuinely so impressed! Amazing script. Thanks for sharing. I was tempted to have a go at doing the enemies as moving blocks too for invader leg movements, but TBH, it would feel like adding lines to someone else’s poem. I am so impressed I would not dream of touching it! Great job. I hope everyone creates a node and a script and tries this out. It literally takes seconds and is so rewarding :slight_smile:

Thank you again!

.
PS You might want to add a line to speed the enemies up as they die.

# Check for collision with enemies
             ...
			if check_collision(projectile, enemy):
				# Enemy hit
				enemy_move_interval -= 0.01
1 Like

That’s a good one!..

And a godot game jam!.. i like that idea!. :heart_eyes:

It’s awesome.

And here is a problem to fix:

  • upon death, β€œPress SPACE to start” stays on the screen after pressing the spacebar.

Line 473:

Change:
	message_label.visible = true
To:
	message_label.visible = false
1 Like

I cannot believe I just spent 4 hours doing this but I have animated the enemies! (Well tween them from one pose to another at least).

Made lots of smaller tweaks too like removing the diagonal movement at the end of the row, making the bunkers a bit taller, increasing the drop at the end of the row, speeding up the enemies as they get lower and as they get killed off (it possibly gets a bit too fast now), moved the player up so it doesn’t overwrite the text and perhaps some other bits you won’t really notice too. The big change of course is the aliens.

I had fun, was a nice distraction! Same rules apply, create a node2D and attach a script and paste this in: (It needs some tidying but after 4 hours of this distraction I really have to get on with some actual work).

Space Invaders code with alterations
extends Node2D

# Constants
const PLAYER_SIZE = Vector2(40, 20)
const ENEMY_SIZE = Vector2(50, 50)
const MOTHERSHIP_SIZE = Vector2(60, 25)
const PROJECTILE_SIZE = Vector2(5, 15)
const BUNKER_BLOCK_SIZE = Vector2(10, 10)
const ENEMY_ROWS = 4
const ENEMY_COLS = 8
const ENEMY_SPACING = Vector2(30, 30)
const ENEMY_MOVE_SPEED = 20
const MIN_ENEMY_MOVE_INTERVAL: float = 0.2
const ENEMY_DROP_AMOUNT = 25
const MAX_PLAYER_PROJECTILES = 3
const PROJECTILE_SPEED = 400
const BUNKER_HEIGHT = 150 #150
const ENEMY_POSES = [
	[
		Vector2(1,0), Vector2(7,0),
		Vector2(2,1), Vector2(6,1), 
		Vector2(2, 2), Vector2(3, 2), Vector2(4, 2), Vector2(5, 2), Vector2(6, 2),
		Vector2(2,3), Vector2(4,3), Vector2(6,3),
		Vector2(0, 4), Vector2(1, 4), Vector2(2, 4), Vector2(3, 4), Vector2(4, 4), Vector2(5, 4), Vector2(6, 4), Vector2(7, 4), Vector2(8, 4), 
		Vector2(0, 5), Vector2(2, 5), Vector2(3, 5), Vector2(4, 5), Vector2(5, 5), Vector2(6, 5), Vector2(8, 5), 
		Vector2(3,6), Vector2(5,6),
	],
	[
		Vector2(3,0), Vector2(5,0),
		Vector2(3,1), Vector2(5,1), 
		Vector2(2, 2), Vector2(3, 2), Vector2(4, 2), Vector2(5, 2), Vector2(6, 2), 
		Vector2(2,3), Vector2(4,3), Vector2(6,3), 
		Vector2(0, 4), Vector2(1, 4), Vector2(2, 4), Vector2(3, 4), Vector2(4, 4), Vector2(5, 4), Vector2(6, 4), Vector2(7, 4), Vector2(8, 4), 
		Vector2(0,3), Vector2(2, 5), Vector2(3, 5), Vector2(4, 5), Vector2(5, 5), Vector2(6, 5), Vector2(8,3),
		Vector2(1,6), Vector2(7,6),
	],
]

# Game Variables
var screen_size
var player_speed = 300
var game_over = false
var game_started = false
var score = 0
var lives = 3
var level = 1
var enemy_shot_chance = 0.0005  # Reduced from 0.002 - chance per enemy per frame to shoot

# Mothership variables
var mothership = null
var mothership_active = false
var mothership_timer = 0
var mothership_interval = 15.0  # Seconds between mothership appearances
var mothership_speed = 100
var mothership_direction = 1

# Nodes
var player
var player_projectiles = []
var enemies = []
var enemy_projectiles = []
var bunkers = []
var score_label
var lives_label
var message_label
var commentary_label

# Enemy movement
var enemy_direction = 1  # 1 for right, -1 for left
var enemy_move_timer = 0
var enemy_move_interval = 0.5  # Time between enemy movements

# Enemy blocks
var enemy_color: Color
var enemy_pixels_dict: Dictionary = {}
var pixels: Array = []
var POSE_SIZE:Vector2 = Vector2(9, 8)
var pixel_size: Vector2 = Vector2(ENEMY_SIZE.x / POSE_SIZE.x, ENEMY_SIZE.y / POSE_SIZE.y)
var should_change_direction: bool = false
var already_moved_vertically: bool = false
var lowest_enemy_y: float
var enemy_tween_time = 0.1

func _ready():
	randomize()
	screen_size = get_viewport_rect().size
	
	#Set important project settings
	get_tree().root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
	get_viewport().canvas_item_default_texture_filter = Viewport.DEFAULT_CANVAS_ITEM_TEXTURE_FILTER_NEAREST

	#Create bg
	var bg = ColorRect.new()
	bg.color = Color.BLACK
	add_child(bg)
	
	bg.size = screen_size
	
	# Create player
	player = ColorRect.new()
	player.color = Color.GREEN
	player.size = PLAYER_SIZE
	player.position = Vector2(screen_size.x / 2 - player.size.x / 2, screen_size.y - player.size.y - 50)
	add_child(player)
	
	# Create score and lives labels
	score_label = Label.new()
	score_label.position = Vector2(20, 20)
	score_label.text = "Score: 0"
	add_child(score_label)
	
	lives_label = Label.new()
	lives_label.position = Vector2(screen_size.x - 100, 20)
	lives_label.text = "Lives: 3"
	add_child(lives_label)
	
	# Create message label (center of screen)
	message_label = Label.new()
	message_label.position = Vector2(screen_size.x / 2 - 100, screen_size.y / 2 - 20)
	message_label.text = "Press SPACE to start"
	add_child(message_label)
	
	# Create commentary label (bottom of screen)
	commentary_label = Label.new()
	commentary_label.position = Vector2(screen_size.x / 2 - 150, screen_size.y - 40)
	commentary_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	commentary_label.size = Vector2(300, 30)
	commentary_label.visible = false
	add_child(commentary_label)


func _process(delta):
	if game_over:
		if Input.is_action_just_pressed("ui_select"):  # Restart game
			reset_game()
		return
	
	if not game_started:
		if Input.is_action_just_pressed("ui_select"):
			game_started = true
			message_label.visible = false
			setup_level(level)
			show_commentary("Space battle begins! Blast 'em!")
		return
	
	# Player movement
	var player_movement = 0
	if Input.is_action_pressed("ui_left"):
		player_movement = -1
	if Input.is_action_pressed("ui_right"):
		player_movement = 1
	
	player.position.x += player_movement * player_speed * delta
	player.position.x = clamp(player.position.x, 0, screen_size.x - player.size.x)
	
	# Player shooting
	if Input.is_action_just_pressed("ui_select") and len(player_projectiles) < MAX_PLAYER_PROJECTILES:
		create_player_projectile()
	
	# Mothership handling
	process_mothership(delta)
	
	# Update player projectiles
	process_player_projectiles(delta)
	
	# Update enemy movement
	process_enemy_movement(delta)
	
	# Random enemy shooting
	for enemy in enemies:
		if randf() < enemy_shot_chance:
			create_enemy_projectile(enemy)
	
	# Update enemy projectiles
	process_enemy_projectiles(delta)


func process_mothership(delta):
	# Mothership timer and creation
	if not mothership_active:
		mothership_timer += delta
		if mothership_timer >= mothership_interval:
			mothership_timer = 0
			create_mothership()
	else:
		# Move mothership
		mothership.position.x += mothership_speed * mothership_direction * delta
		
		# Remove mothership if off screen
		if (mothership_direction > 0 and mothership.position.x > screen_size.x) or \
		   (mothership_direction < 0 and mothership.position.x < -MOTHERSHIP_SIZE.x):
			remove_child(mothership)
			mothership = null
			mothership_active = false


func process_player_projectiles(delta):
	for i in range(player_projectiles.size() - 1, -1, -1):
		var projectile = player_projectiles[i]
		projectile.position.y -= PROJECTILE_SPEED * delta
		
		# Remove projectile if it goes off screen
		if projectile.position.y < -PROJECTILE_SIZE.y:
			remove_child(projectile)
			player_projectiles.remove_at(i)
			continue
		
		# Check for collision with mothership
		if mothership_active and check_collision(projectile, mothership):
			# Mothership hit
			score += 100 * level  # More points for higher levels
			score_label.text = "Score: " + str(score)
			show_commentary("MOTHERSHIP DOWN! " + str(100 * level) + " POINTS!")
			
			# Remove mothership
			remove_child(mothership)
			mothership = null
			mothership_active = false
			
			# Remove projectile
			remove_child(projectile)
			player_projectiles.remove_at(i)
			continue
		
		# Check for collision with enemies
		var enemy_hit = false
		for j in range(enemies.size() - 1, -1, -1):
			var enemy = enemies[j]
			if check_enemy_collision(projectile, enemy):
				# Enemy hit
				score += 10
				score_label.text = "Score: " + str(score)
				enemy_move_interval = max(enemy_move_interval - 0.02, MIN_ENEMY_MOVE_INTERVAL)
				# Show random hit commentary
				show_hit_commentary()
				
				# Remove enemy
				add_particles(enemy.position)
				remove_child(enemy)
				enemies.remove_at(j)
				
				# Remove projectile
				remove_child(projectile)
				player_projectiles.remove_at(i)
				
				enemy_hit = true
				break
		
		if enemy_hit:
			continue
		
		# Check for collision with bunkers
		for j in range(bunkers.size() - 1, -1, -1):
			var bunker_block = bunkers[j]
			if check_collision(projectile, bunker_block):
				# Bunker block hit
				remove_child(bunker_block)
				bunkers.remove_at(j)
				
				# Remove projectile
				remove_child(projectile)
				player_projectiles.remove_at(i)
				break

func add_particles(fire_position: Vector2):
	var particles = CPUParticles2D.new()
	add_child(particles)
	particles.position = fire_position + ENEMY_SIZE / 2
	particles.emitting = false
	particles.amount = 20
	particles.lifetime = 0.5
	particles.one_shot = true
	particles.explosiveness = 1.0
	particles.direction = Vector2(0, -1)
	particles.spread = 180
	particles.gravity = Vector2(0, 98)
	particles.initial_velocity_min = 50
	particles.initial_velocity_max = 100
	particles.scale_amount_min = 2
	particles.scale_amount_max = 4
	particles.color = get_viewport().get_texture().get_image().get_pixel(particles.position.x, particles.position.y)
	particles.emitting = true


func process_enemy_movement(delta):
	# Check if level completed
	if enemies.size() == 0:
		level += 1
		setup_level(level)
		show_commentary("Level " + str(level) + "! Enemies incoming!")
		return
	
	# Check movement timer
	enemy_move_timer += delta
	if enemy_move_timer <= enemy_move_interval:
		return
	enemy_move_timer = 0
	
	# Do appropriate movement type
	should_change_direction = is_enemy_touching_sides()
	if should_change_direction:
		move_enemies_vertically()
	else:
		move_enemies_horizontally()


func is_enemy_touching_sides() -> bool:
	if already_moved_vertically:
		already_moved_vertically = false
		return false
	for enemy in enemies:
		if enemy.position.x <= pixel_size.x or enemy.position.x + ENEMY_SIZE.x + pixel_size.x >= screen_size.x:
			enemy_direction *= -1
			return true
	return false
	

func move_enemies_vertically():
	lowest_enemy_y = 0
	for enemy in enemies:
		animate_enemy_pose(enemy)
		enemy.position.y += ENEMY_DROP_AMOUNT
		# Track lowest enemy
		lowest_enemy_y = max(lowest_enemy_y, enemy.position.y + ENEMY_SIZE.y)

	already_moved_vertically = true
	enemy_move_interval = max(enemy_move_interval - 0.03, MIN_ENEMY_MOVE_INTERVAL)
	
	# Check if movement caused game over
	if lowest_enemy_y >= player.position.y:
		game_over = true
		message_label.text = "Game Over! Press SPACE to restart"
		message_label.visible = true
		show_commentary("They invaded your space! Game over!")


func move_enemies_horizontally():
	should_change_direction = false
	for enemy in enemies:
		animate_enemy_pose(enemy)
		enemy.position.x += ENEMY_MOVE_SPEED * enemy_direction


func animate_enemy_pose(EnemyNode: Node2D):
	var current_pose: int = EnemyNode.get_meta("pose")
	var new_pose: int = (current_pose + 1) % ENEMY_POSES.size()
	EnemyNode.set_meta("pose", new_pose)
	var next_pose = ENEMY_POSES[new_pose]
	pixels = enemy_pixels_dict[EnemyNode]
	for i in range(pixels.size()):
		if i < next_pose.size():
			if enemy_move_interval < enemy_tween_time:
				pixels[i].position = next_pose[i] * pixel_size
			else:
				var tween = EnemyNode.create_tween()
				tween.tween_property(pixels[i], "position", position + (next_pose[i] * pixel_size), enemy_tween_time)
				EnemyNode.set_meta("active_tween", tween)
		else:
			pixels[i].visible = false


func process_enemy_projectiles(delta):
	for i in range(enemy_projectiles.size() - 1, -1, -1):
		var projectile = enemy_projectiles[i]
		projectile.position.y += PROJECTILE_SPEED * delta
		
		# Remove projectile if it goes off screen
		if projectile.position.y > screen_size.y:
			remove_child(projectile)
			enemy_projectiles.remove_at(i)
			continue
		
		# Check for collision with player
		if check_collision(projectile, player):
			# Player hit
			lives -= 1
			lives_label.text = "Lives: " + str(lives)
			
			# Show hit commentary
			show_player_hit_commentary()
			
			# Remove projectile
			remove_child(projectile)
			enemy_projectiles.remove_at(i)
			
			if lives <= 0:
				game_over = true
				message_label.text = "Game Over! Press SPACE to restart"
				message_label.visible = true
				show_commentary("You're out of ships! Game over!")
			
			continue
		
		# Check for collision with bunkers
		for j in range(bunkers.size() - 1, -1, -1):
			var bunker_block = bunkers[j]
			if check_collision(projectile, bunker_block):
				# Bunker block hit
				remove_child(bunker_block)
				bunkers.remove_at(j)
				
				# Remove projectile
				remove_child(projectile)
				enemy_projectiles.remove_at(i)
				break

func create_mothership():
	mothership = ColorRect.new()
	mothership.color = Color.PURPLE
	mothership.size = MOTHERSHIP_SIZE
	
	# Randomize direction
	mothership_direction = 1 if randf() > 0.5 else -1
	
	if mothership_direction > 0:
		mothership.position = Vector2(-MOTHERSHIP_SIZE.x, 40)
	else:
		mothership.position = Vector2(screen_size.x, 40)
	
	add_child(mothership)
	mothership_active = true
	show_commentary("MOTHERSHIP INCOMING!")

func create_bunkers():
	# Clear any existing bunkers
	for bunker in bunkers:
		remove_child(bunker)
	bunkers.clear()
	
	# Create 4 bunkers evenly spaced
	var bunker_width = 5 * BUNKER_BLOCK_SIZE.x
	var spacing = screen_size.x / 5
	
	for b in range(4):
		var bunker_x = spacing * (b + 1) - bunker_width / 2
		var bunker_y = screen_size.y - BUNKER_HEIGHT
		
		# Create bunker with a shape like:
		#  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
		# β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
		# β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
		# β–ˆβ–ˆ   β–ˆβ–ˆ
		
		# Top row (5 blocks)
		for i in range(5):
			create_bunker_block(Vector2(bunker_x + i * BUNKER_BLOCK_SIZE.x, bunker_y))
		
		# Middle row (7 blocks)
		for i in range(7):
			var x = bunker_x + (i - 1) * BUNKER_BLOCK_SIZE.x
			create_bunker_block(Vector2(x, bunker_y + BUNKER_BLOCK_SIZE.y))
		
		# Additional Middle row (7 blocks)
		for i in range(7):
			var x = bunker_x + (i - 1) * BUNKER_BLOCK_SIZE.x
			create_bunker_block(Vector2(x, bunker_y + 2 * BUNKER_BLOCK_SIZE.y))
		
		# Bottom row (7 blocks with gap in middle)
		for i in range(7):
			if i < 2 or i > 4:  # Skip middle block to create doorway
				var x = bunker_x + (i - 1) * BUNKER_BLOCK_SIZE.x
				create_bunker_block(Vector2(x, bunker_y + 3 * BUNKER_BLOCK_SIZE.y))

func create_bunker_block(block_position):
	var block = ColorRect.new()
	block.color = Color(0, 0.5, 0)  # Dark green
	block.size = BUNKER_BLOCK_SIZE
	block.position = block_position
	add_child(block)
	bunkers.append(block)

func setup_level(level_num):
	# Clear any existing enemies and projectiles
	for enemy in enemies:
		remove_child(enemy)
	enemies.clear()
	
	for projectile in player_projectiles:
		remove_child(projectile)
	player_projectiles.clear()
	
	for projectile in enemy_projectiles:
		remove_child(projectile)
	enemy_projectiles.clear()
	
	if mothership_active and mothership != null:
		remove_child(mothership)
		mothership = null
		mothership_active = false
	
	# Create bunkers
	create_bunkers()
	
	# Adjust difficulty based on level
	enemy_move_interval = max(MIN_ENEMY_MOVE_INTERVAL, 0.5 - (level_num - 1) * 0.05)
	enemy_shot_chance = min(0.003, 0.0005 + (level_num - 1) * 0.0003)  # Still much lower than original
	
	# Create enemies
	var start_x = (screen_size.x - (ENEMY_COLS * (ENEMY_SIZE.x + ENEMY_SPACING.x))) / 2 + ENEMY_SIZE.x / 2
	var start_y = 70
	
	for row in range(ENEMY_ROWS):
		if row == 0:
			enemy_color = Color.RED
		elif row == 1:
			enemy_color = Color.ORANGE
		elif row == 2:
			enemy_color = Color.YELLOW
		else:
			enemy_color = Color(1, 0.5, 0)  # Orange-ish
			
		for col in range(ENEMY_COLS):
			var enemy = create_enemy_block(row)
			#
			#var enemy = ColorRect.new()
			#
			
			#enemy.size = ENEMY_SIZE
			enemy.position = Vector2(
				start_x + col * (ENEMY_SIZE.x + ENEMY_SPACING.x),
				start_y + row * (ENEMY_SIZE.y + ENEMY_SPACING.y)
			)
			add_child(enemy)
			enemies.append(enemy)
	
	# Reset enemy movement
	enemy_direction = 1
	enemy_move_timer = 0
	
	# Reset mothership timer
	mothership_timer = 0


func create_enemy_block(row):
	# Create enemy with two 7x9 shapes like this:
	#  β–ˆ    β–ˆ          β–ˆ β–ˆ    
	#   β–ˆ  β–ˆ           β–ˆ β–ˆ   
	#   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ         β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  
	#   β–ˆ β–ˆ β–ˆ       β–ˆ β–ˆ β–ˆ β–ˆ β–ˆ 
	# β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
	# β–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆ       β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 
	#    β–ˆ β–ˆ         β–ˆ     β–ˆ    
	
	# Different colors based on row
	var enemy_node = Node2D.new()
	var pose: int = row % 2
	enemy_node.set_meta("pose", pose)
	pixels = []

	for pos in ENEMY_POSES[pose]:
		var rect = ColorRect.new()
		rect.position = pos * pixel_size
		rect.size = pixel_size
		rect.color = enemy_color
		enemy_node.add_child(rect)
		pixels.append(rect)
	
	enemy_pixels_dict[enemy_node] = pixels
	return enemy_node


func create_player_projectile():
	var projectile = ColorRect.new()
	projectile.color = Color.GREEN
	projectile.size = PROJECTILE_SIZE
	projectile.position = Vector2(
		player.position.x + player.size.x / 2 - PROJECTILE_SIZE.x / 2,
		player.position.y - PROJECTILE_SIZE.y
	)
	add_child(projectile)
	player_projectiles.append(projectile)

func create_enemy_projectile(enemy):
	var projectile = ColorRect.new()
	projectile.color = Color.RED
	projectile.size = PROJECTILE_SIZE
	projectile.position = Vector2(
		enemy.position.x + ENEMY_SIZE.x / 2 - PROJECTILE_SIZE.x / 2,
		enemy.position.y + ENEMY_SIZE.y
	)
	add_child(projectile)
	enemy_projectiles.append(projectile)

func check_collision(rect1, rect2):
	return (
		rect1.position.x < rect2.position.x + rect2.size.x and
		rect1.position.x + rect1.size.x > rect2.position.x and
		rect1.position.y < rect2.position.y + rect2.size.y and
		rect1.position.y + rect1.size.y > rect2.position.y
	)

func check_enemy_collision(rect1, EnemyNode):
	return (
		rect1.position.x < EnemyNode.position.x + ENEMY_SIZE.x and
		rect1.position.x + rect1.size.x > EnemyNode.position.x and
		rect1.position.y < EnemyNode.position.y + ENEMY_SIZE.y and
		rect1.position.y + rect1.size.y > EnemyNode.position.y
	)

func reset_game():
	score = 0
	lives = 3
	level = 1
	score_label.text = "Score: 0"
	lives_label.text = "Lives: 3"
	game_over = false
	message_label.text = "Press SPACE to start"
	message_label.visible = false
	
	# Reset player position
	player.position = Vector2(screen_size.x / 2 - player.size.x / 2, screen_size.y - player.size.y - 20)
	
	# Clear any existing enemies and projectiles
	for enemy in enemies:
		remove_child(enemy)
	enemies.clear()
	
	for projectile in player_projectiles:
		remove_child(projectile)
	player_projectiles.clear()
	
	for projectile in enemy_projectiles:
		remove_child(projectile)
	enemy_projectiles.clear()
	
	if mothership_active and mothership != null:
		remove_child(mothership)
		mothership = null
		mothership_active = false
	
	for bunker in bunkers:
		remove_child(bunker)
	bunkers.clear()
	
	# Reset mothership timer
	mothership_timer = 0
	mothership_active = false

func show_commentary(text):
	commentary_label.text = text
	commentary_label.visible = true
	
	# Auto-hide commentary after a delay
	var timer = get_tree().create_timer(2.0)
	await timer.timeout
	if is_instance_valid(commentary_label):
		commentary_label.visible = false

func show_hit_commentary():
	var comments = [
		"Boom! Got one!",
		"Alien down!",
		"Nice shot!",
		"Target eliminated!",
		"That's a hit!",
		"Blasted!",
		"They're dropping like flies!",
		"One less invader!",
		"Direct hit!",
		"Alien go splat!"
	]
	
	# Add special comments for scoring milestones
	if score % 100 == 0:
		show_commentary("Score: " + str(score) + "! Keep it up!")
		return
	
	# Add special comments for few enemies left
	if enemies.size() <= 3 and enemies.size() > 0:
		comments.append("Almost got 'em all!")
		comments.append("Just " + str(enemies.size()) + " more to go!")
		comments.append("They're almost extinct!")
	
	show_commentary(comments[randi() % comments.size()])

func show_player_hit_commentary():
	var comments = [
		"Ouch! You've been hit!",
		"Shields damaged!",
		"They got you!",
		"Ship hit! " + str(lives) + " left!",
		"Watch out for those lasers!",
		"Hit detected! Keep fighting!",
		"That's gonna leave a mark!",
		"Your ship's on fire!",
		"Engines damaged!",
		"Critical hit!"
	]
	
	# Add special comments based on lives left
	if lives == 1:
		comments = [
			"Last life! Be careful!",
			"One more hit and you're done!",
			"Shields critical!",
			"Final ship remaining!",
			"Don't die now!"
		]
	
	show_commentary(comments[randi() % comments.size()])

PS The aliens not colliding with bunkers is annoying me though, but honestly, I can’t spend any more time on this, my own game is beckoning :slight_smile:

PPS Now with particles and @KingGD ammendments below!

5 Likes

Great job! Thanks for it!

Also, I am seeing for the first time someone got 10 likes within 24 hrs.

Nice job! I can`t believe how you animated the sprites :eyes:

1 Like

I just noticed a prob or two (little things really) just amending it now.

Edited it to a few items fixed. Glad you like it! If you change the enemy_tween_time to a bit higher it looks quite funny :slight_smile:

1 Like

Hello Everyone, here is my edited codes, if you want to add a bg and adjust the screen size, you can add these in the codes:

func _ready():
	randomize()
	screen_size = get_viewport_rect().size
	
	#Set important project settings
	get_tree().root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
	get_viewport().canvas_item_default_texture_filter = Viewport.DEFAULT_CANVAS_ITEM_TEXTURE_FILTER_NEAREST

	#Create bg
	var bg = ColorRect.new()
	bg.color = Color.BLACK
	add_child(bg)
	
	bg.size = screen_size

... #Rest of codes

Have a nice day!

1 Like

So if anyone is still interested, here it is with @KingGD excellent full screen change, and particles when the enemy explodes :slight_smile:

Edit: Changed the original code post by me above instead of taking up so much room.

Nice particles! But I recommend to set the particles color based on the exact enemy color. Here how you can do it:

#Replace the current line
particles.color = Color.GREEN

#to this
particles.color = get_viewport().get_texture().get_image().get_pixel(particles.position.x, particles.position.y)

@pauldrewett You can edit your reply now :slight_smile:

Otherwise, I really appreciate @redflare for it! I like the random comment system.

1 Like

Oh very nice! I did want to do this but didn’t know how I could do it easily and quikly (was thinking about adding another meta value when the enemy was created for the colour code or something like that). This is brilliant! Have edited my reply above. Great touch, thank you.

1 Like

hey guys, collapse your long code to [detail] block, like this:

Space Invader Code version x.x.x
#code here

it will be more tidy

or directly segment the code via [detail]

3 Likes

Maybe I am replying here too much :sweat_smile:

But I think its my last reply here. I added a stary space background and combined @pauldrewett particles.

Here is the final codes
extends Node2D

# Constants
const PLAYER_SIZE = Vector2(40, 20)
const ENEMY_SIZE = Vector2(50, 50)
const MOTHERSHIP_SIZE = Vector2(60, 25)
const PROJECTILE_SIZE = Vector2(5, 15)
const BUNKER_BLOCK_SIZE = Vector2(10, 10)
const ENEMY_ROWS = 4
const ENEMY_COLS = 8
const ENEMY_SPACING = Vector2(30, 30)
const ENEMY_MOVE_SPEED = 20
const MIN_ENEMY_MOVE_INTERVAL: float = 0.2
const ENEMY_DROP_AMOUNT = 25
const MAX_PLAYER_PROJECTILES = 3
const PROJECTILE_SPEED = 400
const BUNKER_HEIGHT = 150 #150
const ENEMY_POSES = [
	[
		Vector2(1,0), Vector2(7,0),
		Vector2(2,1), Vector2(6,1), 
		Vector2(2, 2), Vector2(3, 2), Vector2(4, 2), Vector2(5, 2), Vector2(6, 2),
		Vector2(2,3), Vector2(4,3), Vector2(6,3),
		Vector2(0, 4), Vector2(1, 4), Vector2(2, 4), Vector2(3, 4), Vector2(4, 4), Vector2(5, 4), Vector2(6, 4), Vector2(7, 4), Vector2(8, 4), 
		Vector2(0, 5), Vector2(2, 5), Vector2(3, 5), Vector2(4, 5), Vector2(5, 5), Vector2(6, 5), Vector2(8, 5), 
		Vector2(3,6), Vector2(5,6),
	],
	[
		Vector2(3,0), Vector2(5,0),
		Vector2(3,1), Vector2(5,1), 
		Vector2(2, 2), Vector2(3, 2), Vector2(4, 2), Vector2(5, 2), Vector2(6, 2), 
		Vector2(2,3), Vector2(4,3), Vector2(6,3), 
		Vector2(0, 4), Vector2(1, 4), Vector2(2, 4), Vector2(3, 4), Vector2(4, 4), Vector2(5, 4), Vector2(6, 4), Vector2(7, 4), Vector2(8, 4), 
		Vector2(0,3), Vector2(2, 5), Vector2(3, 5), Vector2(4, 5), Vector2(5, 5), Vector2(6, 5), Vector2(8,3),
		Vector2(1,6), Vector2(7,6),
	],
]

# Game Variables
var screen_size
var player_speed = 300
var game_over = false
var game_started = false
var score = 0
var lives = 3
var level = 1
var enemy_shot_chance = 0.0005  # Reduced from 0.002 - chance per enemy per frame to shoot

# Mothership variables
var mothership = null
var mothership_active = false
var mothership_timer = 0
var mothership_interval = 15.0  # Seconds between mothership appearances
var mothership_speed = 100
var mothership_direction = 1

# Nodes
var player
var player_projectiles = []
var enemies = []
var enemy_projectiles = []
var bunkers = []
var score_label
var lives_label
var message_label
var commentary_label

# Enemy movement
var enemy_direction = 1  # 1 for right, -1 for left
var enemy_move_timer = 0
var enemy_move_interval = 0.5  # Time between enemy movements

# Enemy blocks
var enemy_color: Color
var enemy_pixels_dict: Dictionary = {}
var pixels: Array = []
var POSE_SIZE:Vector2 = Vector2(9, 8)
var pixel_size: Vector2 = Vector2(ENEMY_SIZE.x / POSE_SIZE.x, ENEMY_SIZE.y / POSE_SIZE.y)
var should_change_direction: bool = false
var already_moved_vertically: bool = false
var lowest_enemy_y: float
var enemy_tween_time = 0.1

var stars: Array[Vector2] = []

func _ready():
	randomize()
	screen_size = get_viewport_rect().size
	
	generate_stars()
	queue_redraw()
	
	#Set important project settings
	get_tree().root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
	get_viewport().canvas_item_default_texture_filter = Viewport.DEFAULT_CANVAS_ITEM_TEXTURE_FILTER_NEAREST
	
	# Create player
	player = ColorRect.new()
	player.color = Color.GREEN
	player.size = PLAYER_SIZE
	player.position = Vector2(screen_size.x / 2 - player.size.x / 2, screen_size.y - player.size.y - 50)
	add_child(player)
	
	# Create score and lives labels
	score_label = Label.new()
	score_label.position = Vector2(20, 20)
	score_label.text = "Score: 0"
	add_child(score_label)
	
	lives_label = Label.new()
	lives_label.position = Vector2(screen_size.x - 100, 20)
	lives_label.text = "Lives: 3"
	add_child(lives_label)
	
	# Create message label (center of screen)
	message_label = Label.new()
	message_label.position = Vector2(screen_size.x / 2 - 100, screen_size.y / 2 - 20)
	message_label.text = "Press SPACE to start"
	add_child(message_label)
	
	# Create commentary label (bottom of screen)
	commentary_label = Label.new()
	commentary_label.position = Vector2(screen_size.x / 2 - 150, screen_size.y - 40)
	commentary_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	commentary_label.size = Vector2(300, 30)
	commentary_label.visible = false
	add_child(commentary_label)

func generate_stars():
	stars.clear()
	
	for i in range(200):
		var star_position = Vector2(randf_range(0, screen_size.x), randf_range(0, screen_size.y))
		stars.append(star_position)

func _draw():
	draw_rect(Rect2(Vector2.ZERO, screen_size), Color.BLACK, true)
	
	for star in stars:
		draw_circle(star, randf_range(0.6, 1.2), Color.WHITE)

func _process(delta):
	if game_over:
		if Input.is_action_just_pressed("ui_select"):  # Restart game
			reset_game()
		return
	
	if not game_started:
		if Input.is_action_just_pressed("ui_select"):
			game_started = true
			message_label.visible = false
			setup_level(level)
			show_commentary("Space battle begins! Blast 'em!")
		return
	
	# Player movement
	var player_movement = 0
	if Input.is_action_pressed("ui_left"):
		player_movement = -1
	if Input.is_action_pressed("ui_right"):
		player_movement = 1
	
	player.position.x += player_movement * player_speed * delta
	player.position.x = clamp(player.position.x, 0, screen_size.x - player.size.x)
	
	# Player shooting
	if Input.is_action_just_pressed("ui_select") and len(player_projectiles) < MAX_PLAYER_PROJECTILES:
		create_player_projectile()
	
	# Mothership handling
	process_mothership(delta)
	
	# Update player projectiles
	process_player_projectiles(delta)
	
	# Update enemy movement
	process_enemy_movement(delta)
	
	# Random enemy shooting
	for enemy in enemies:
		if randf() < enemy_shot_chance:
			create_enemy_projectile(enemy)
	
	# Update enemy projectiles
	process_enemy_projectiles(delta)


func process_mothership(delta):
	# Mothership timer and creation
	if not mothership_active:
		mothership_timer += delta
		if mothership_timer >= mothership_interval:
			mothership_timer = 0
			create_mothership()
	else:
		# Move mothership
		mothership.position.x += mothership_speed * mothership_direction * delta
		
		# Remove mothership if off screen
		if (mothership_direction > 0 and mothership.position.x > screen_size.x) or \
		   (mothership_direction < 0 and mothership.position.x < -MOTHERSHIP_SIZE.x):
			remove_child(mothership)
			mothership = null
			mothership_active = false


func process_player_projectiles(delta):
	for i in range(player_projectiles.size() - 1, -1, -1):
		var projectile = player_projectiles[i]
		projectile.position.y -= PROJECTILE_SPEED * delta
		
		# Remove projectile if it goes off screen
		if projectile.position.y < -PROJECTILE_SIZE.y:
			remove_child(projectile)
			player_projectiles.remove_at(i)
			continue
		
		# Check for collision with mothership
		if mothership_active and check_collision(projectile, mothership):
			# Mothership hit
			score += 100 * level  # More points for higher levels
			score_label.text = "Score: " + str(score)
			show_commentary("MOTHERSHIP DOWN! " + str(100 * level) + " POINTS!")
			
			# Remove mothership
			remove_child(mothership)
			mothership = null
			mothership_active = false
			
			# Remove projectile
			remove_child(projectile)
			player_projectiles.remove_at(i)
			continue
		
		# Check for collision with enemies
		var enemy_hit = false
		for j in range(enemies.size() - 1, -1, -1):
			var enemy = enemies[j]
			if check_enemy_collision(projectile, enemy):
				# Enemy hit
				score += 10
				score_label.text = "Score: " + str(score)
				enemy_move_interval = max(enemy_move_interval - 0.02, MIN_ENEMY_MOVE_INTERVAL)
				# Show random hit commentary
				show_hit_commentary()
				# Remove enemy
				add_particles(enemy.position)
				remove_child(enemy)
				enemies.remove_at(j)
				
				# Remove projectile
				remove_child(projectile)
				player_projectiles.remove_at(i)
				
				enemy_hit = true
				break
		
		if enemy_hit:
			continue
		
		# Check for collision with bunkers
		for j in range(bunkers.size() - 1, -1, -1):
			var bunker_block = bunkers[j]
			if check_collision(projectile, bunker_block):
				# Bunker block hit
				remove_child(bunker_block)
				bunkers.remove_at(j)
				
				# Remove projectile
				remove_child(projectile)
				player_projectiles.remove_at(i)
				break

func add_particles(fire_position: Vector2):
	var particles = CPUParticles2D.new()
	add_child(particles)
	particles.position = fire_position + ENEMY_SIZE / 2
	particles.emitting = false
	particles.amount = 20
	particles.lifetime = 0.5
	particles.one_shot = true
	particles.explosiveness = 1.0
	particles.direction = Vector2(0, -1)
	particles.spread = 180
	particles.gravity = Vector2(0, 98)
	particles.initial_velocity_min = 50
	particles.initial_velocity_max = 100
	particles.scale_amount_min = 2
	particles.scale_amount_max = 4
	particles.color = get_viewport().get_texture().get_image().get_pixel(particles.position.x, particles.position.y)
	particles.emitting = true


func process_enemy_movement(delta):
	# Check if level completed
	if enemies.size() == 0:
		level += 1
		setup_level(level)
		show_commentary("Level " + str(level) + "! Enemies incoming!")
		return
	
	# Check movement timer
	enemy_move_timer += delta
	if enemy_move_timer <= enemy_move_interval:
		return
	enemy_move_timer = 0
	
	# Do appropriate movement type
	should_change_direction = is_enemy_touching_sides()
	if should_change_direction:
		move_enemies_vertically()
	else:
		move_enemies_horizontally()


func is_enemy_touching_sides() -> bool:
	if already_moved_vertically:
		already_moved_vertically = false
		return false
	for enemy in enemies:
		if enemy.position.x <= pixel_size.x or enemy.position.x + ENEMY_SIZE.x + pixel_size.x >= screen_size.x:
			enemy_direction *= -1
			return true
	return false
	

func move_enemies_vertically():
	lowest_enemy_y = 0
	for enemy in enemies:
		animate_enemy_pose(enemy)
		enemy.position.y += ENEMY_DROP_AMOUNT
		# Track lowest enemy
		lowest_enemy_y = max(lowest_enemy_y, enemy.position.y + ENEMY_SIZE.y)

	already_moved_vertically = true
	enemy_move_interval = max(enemy_move_interval - 0.03, MIN_ENEMY_MOVE_INTERVAL)
	
	# Check if movement caused game over
	if lowest_enemy_y >= player.position.y:
		game_over = true
		message_label.text = "Game Over! Press SPACE to restart"
		message_label.visible = true
		show_commentary("They invaded your space! Game over!")


func move_enemies_horizontally():
	should_change_direction = false
	for enemy in enemies:
		animate_enemy_pose(enemy)
		enemy.position.x += ENEMY_MOVE_SPEED * enemy_direction


func animate_enemy_pose(EnemyNode: Node2D):
	var current_pose: int = EnemyNode.get_meta("pose")
	var new_pose: int = (current_pose + 1) % ENEMY_POSES.size()
	EnemyNode.set_meta("pose", new_pose)
	var next_pose = ENEMY_POSES[new_pose]
	pixels = enemy_pixels_dict[EnemyNode]
	for i in range(pixels.size()):
		if i < next_pose.size():
			if enemy_move_interval < enemy_tween_time:
				pixels[i].position = next_pose[i] * pixel_size
			else:
				var tween = EnemyNode.create_tween()
				tween.tween_property(pixels[i], "position", position + (next_pose[i] * pixel_size), enemy_tween_time)
				EnemyNode.set_meta("active_tween", tween)
		else:
			pixels[i].visible = false


func process_enemy_projectiles(delta):
	for i in range(enemy_projectiles.size() - 1, -1, -1):
		var projectile = enemy_projectiles[i]
		projectile.position.y += PROJECTILE_SPEED * delta
		
		# Remove projectile if it goes off screen
		if projectile.position.y > screen_size.y:
			remove_child(projectile)
			enemy_projectiles.remove_at(i)
			continue
		
		# Check for collision with player
		if check_collision(projectile, player):
			# Player hit
			lives -= 1
			lives_label.text = "Lives: " + str(lives)
			
			# Show hit commentary
			show_player_hit_commentary()
			
			# Remove projectile
			remove_child(projectile)
			enemy_projectiles.remove_at(i)
			
			if lives <= 0:
				game_over = true
				message_label.text = "Game Over! Press SPACE to restart"
				message_label.visible = true
				show_commentary("You're out of ships! Game over!")
			
			continue
		
		# Check for collision with bunkers
		for j in range(bunkers.size() - 1, -1, -1):
			var bunker_block = bunkers[j]
			if check_collision(projectile, bunker_block):
				# Bunker block hit
				remove_child(bunker_block)
				bunkers.remove_at(j)
				
				# Remove projectile
				remove_child(projectile)
				enemy_projectiles.remove_at(i)
				break

func create_mothership():
	mothership = ColorRect.new()
	mothership.color = Color.PURPLE
	mothership.size = MOTHERSHIP_SIZE
	
	# Randomize direction
	mothership_direction = 1 if randf() > 0.5 else -1
	
	if mothership_direction > 0:
		mothership.position = Vector2(-MOTHERSHIP_SIZE.x, 40)
	else:
		mothership.position = Vector2(screen_size.x, 40)
	
	add_child(mothership)
	mothership_active = true
	show_commentary("MOTHERSHIP INCOMING!")

func create_bunkers():
	# Clear any existing bunkers
	for bunker in bunkers:
		remove_child(bunker)
	bunkers.clear()
	
	# Create 4 bunkers evenly spaced
	var bunker_width = 5 * BUNKER_BLOCK_SIZE.x
	var spacing = screen_size.x / 5
	
	for b in range(4):
		var bunker_x = spacing * (b + 1) - bunker_width / 2
		var bunker_y = screen_size.y - BUNKER_HEIGHT
		
		# Create bunker with a shape like:
		#  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
		# β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
		# β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
		# β–ˆβ–ˆ   β–ˆβ–ˆ
		
		# Top row (5 blocks)
		for i in range(5):
			create_bunker_block(Vector2(bunker_x + i * BUNKER_BLOCK_SIZE.x, bunker_y))
		
		# Middle row (7 blocks)
		for i in range(7):
			var x = bunker_x + (i - 1) * BUNKER_BLOCK_SIZE.x
			create_bunker_block(Vector2(x, bunker_y + BUNKER_BLOCK_SIZE.y))
		
		# Additional Middle row (7 blocks)
		for i in range(7):
			var x = bunker_x + (i - 1) * BUNKER_BLOCK_SIZE.x
			create_bunker_block(Vector2(x, bunker_y + 2 * BUNKER_BLOCK_SIZE.y))
		
		# Bottom row (7 blocks with gap in middle)
		for i in range(7):
			if i < 2 or i > 4:  # Skip middle block to create doorway
				var x = bunker_x + (i - 1) * BUNKER_BLOCK_SIZE.x
				create_bunker_block(Vector2(x, bunker_y + 3 * BUNKER_BLOCK_SIZE.y))

func create_bunker_block(block_position):
	var block = ColorRect.new()
	block.color = Color(0, 0.5, 0)  # Dark green
	block.size = BUNKER_BLOCK_SIZE
	block.position = block_position
	add_child(block)
	bunkers.append(block)

func setup_level(level_num):
	# Clear any existing enemies and projectiles
	for enemy in enemies:
		remove_child(enemy)
	enemies.clear()
	
	for projectile in player_projectiles:
		remove_child(projectile)
	player_projectiles.clear()
	
	for projectile in enemy_projectiles:
		remove_child(projectile)
	enemy_projectiles.clear()
	
	if mothership_active and mothership != null:
		remove_child(mothership)
		mothership = null
		mothership_active = false
	
	# Create bunkers
	create_bunkers()
	
	# Adjust difficulty based on level
	enemy_move_interval = max(MIN_ENEMY_MOVE_INTERVAL, 0.5 - (level_num - 1) * 0.05)
	enemy_shot_chance = min(0.003, 0.0005 + (level_num - 1) * 0.0003)  # Still much lower than original
	
	# Create enemies
	var start_x = (screen_size.x - (ENEMY_COLS * (ENEMY_SIZE.x + ENEMY_SPACING.x))) / 2 + ENEMY_SIZE.x / 2
	var start_y = 70
	
	for row in range(ENEMY_ROWS):
		if row == 0:
			enemy_color = Color.RED
		elif row == 1:
			enemy_color = Color.ORANGE
		elif row == 2:
			enemy_color = Color.YELLOW
		else:
			enemy_color = Color(1, 0.5, 0)  # Orange-ish
			
		for col in range(ENEMY_COLS):
			var enemy = create_enemy_block(row)
			#
			#var enemy = ColorRect.new()
			#
			
			#enemy.size = ENEMY_SIZE
			enemy.position = Vector2(
				start_x + col * (ENEMY_SIZE.x + ENEMY_SPACING.x),
				start_y + row * (ENEMY_SIZE.y + ENEMY_SPACING.y)
			)
			add_child(enemy)
			enemies.append(enemy)
	
	# Reset enemy movement
	enemy_direction = 1
	enemy_move_timer = 0
	
	# Reset mothership timer
	mothership_timer = 0


func create_enemy_block(row):
	# Create enemy with two 7x9 shapes like this:
	#  β–ˆ    β–ˆ          β–ˆ β–ˆ    
	#   β–ˆ  β–ˆ           β–ˆ β–ˆ   
	#   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ         β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  
	#   β–ˆ β–ˆ β–ˆ       β–ˆ β–ˆ β–ˆ β–ˆ β–ˆ 
	# β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
	# β–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆ       β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 
	#    β–ˆ β–ˆ         β–ˆ     β–ˆ    
	
	# Different colors based on row
	var enemy_node = Node2D.new()
	var pose: int = row % 2
	enemy_node.set_meta("pose", pose)
	pixels = []

	for pos in ENEMY_POSES[pose]:
		var rect = ColorRect.new()
		rect.position = pos * pixel_size
		rect.size = pixel_size
		rect.color = enemy_color
		enemy_node.add_child(rect)
		pixels.append(rect)
	
	enemy_pixels_dict[enemy_node] = pixels
	return enemy_node


func create_player_projectile():
	var projectile = ColorRect.new()
	projectile.color = Color.GREEN
	projectile.size = PROJECTILE_SIZE
	projectile.position = Vector2(
		player.position.x + player.size.x / 2 - PROJECTILE_SIZE.x / 2,
		player.position.y - PROJECTILE_SIZE.y
	)
	add_child(projectile)
	player_projectiles.append(projectile)

func create_enemy_projectile(enemy):
	var projectile = ColorRect.new()
	projectile.color = Color.RED
	projectile.size = PROJECTILE_SIZE
	projectile.position = Vector2(
		enemy.position.x + ENEMY_SIZE.x / 2 - PROJECTILE_SIZE.x / 2,
		enemy.position.y + ENEMY_SIZE.y
	)
	add_child(projectile)
	enemy_projectiles.append(projectile)

func check_collision(rect1, rect2):
	return (
		rect1.position.x < rect2.position.x + rect2.size.x and
		rect1.position.x + rect1.size.x > rect2.position.x and
		rect1.position.y < rect2.position.y + rect2.size.y and
		rect1.position.y + rect1.size.y > rect2.position.y
	)

func check_enemy_collision(rect1, EnemyNode):
	return (
		rect1.position.x < EnemyNode.position.x + ENEMY_SIZE.x and
		rect1.position.x + rect1.size.x > EnemyNode.position.x and
		rect1.position.y < EnemyNode.position.y + ENEMY_SIZE.y and
		rect1.position.y + rect1.size.y > EnemyNode.position.y
	)

func reset_game():
	score = 0
	lives = 3
	level = 1
	score_label.text = "Score: 0"
	lives_label.text = "Lives: 3"
	game_over = false
	message_label.text = "Press SPACE to start"
	message_label.visible = false
	
	# Reset player position
	player.position = Vector2(screen_size.x / 2 - player.size.x / 2, screen_size.y - player.size.y - 20)
	
	# Clear any existing enemies and projectiles
	for enemy in enemies:
		remove_child(enemy)
	enemies.clear()
	
	for projectile in player_projectiles:
		remove_child(projectile)
	player_projectiles.clear()
	
	for projectile in enemy_projectiles:
		remove_child(projectile)
	enemy_projectiles.clear()
	
	if mothership_active and mothership != null:
		remove_child(mothership)
		mothership = null
		mothership_active = false
	
	for bunker in bunkers:
		remove_child(bunker)
	bunkers.clear()
	
	# Reset mothership timer
	mothership_timer = 0
	mothership_active = false

func show_commentary(text):
	commentary_label.text = text
	commentary_label.visible = true
	
	# Auto-hide commentary after a delay
	var timer = get_tree().create_timer(2.0)
	await timer.timeout
	if is_instance_valid(commentary_label):
		commentary_label.visible = false

func show_hit_commentary():
	var comments = [
		"Boom! Got one!",
		"Alien down!",
		"Nice shot!",
		"Target eliminated!",
		"That's a hit!",
		"Blasted!",
		"They're dropping like flies!",
		"One less invader!",
		"Direct hit!",
		"Alien go splat!"
	]
	
	# Add special comments for scoring milestones
	if score % 100 == 0:
		show_commentary("Score: " + str(score) + "! Keep it up!")
		return
	
	# Add special comments for few enemies left
	if enemies.size() <= 3 and enemies.size() > 0:
		comments.append("Almost got 'em all!")
		comments.append("Just " + str(enemies.size()) + " more to go!")
		comments.append("They're almost extinct!")
	
	show_commentary(comments[randi() % comments.size()])

func show_player_hit_commentary():
	var comments = [
		"Ouch! You've been hit!",
		"Shields damaged!",
		"They got you!",
		"Ship hit! " + str(lives) + " left!",
		"Watch out for those lasers!",
		"Hit detected! Keep fighting!",
		"That's gonna leave a mark!",
		"Your ship's on fire!",
		"Engines damaged!",
		"Critical hit!"
	]
	
	# Add special comments based on lives left
	if lives == 1:
		comments = [
			"Last life! Be careful!",
			"One more hit and you're done!",
			"Shields critical!",
			"Final ship remaining!",
			"Don't die now!"
		]
	
	show_commentary(comments[randi() % comments.size()])

@EX74 Thanks! I did not know before we can do it.

@pauldrewett You can edit your codes too! If you want to make this change, just look at my _ready codes and generate_stars function.

4 Likes

@EX74 I never knew that either! Thanks, that is much better!

2 Likes

@KingGD Great addition. Your version can be the final one on here for now, people will scroll down and see it. The forum software is complaining about all my edits :slight_smile:

It is amazing how much fun it is polishing such an apparently simple code example. The animated aliens was a challenge I couldn’t resist given how @redflare had done all the ground work and I was inspired by his use of coloured rectangles for the parts.

Perhaps in the future I will do a pac_man single script inspired by this space_invaders.

If anyone wants to add to this I hope someone thinks up a way to add some basic beeps and chirps for the movement and lasers (and fixes that pesky aliens hitting the bunkers thing). Also the player and the mother ship need a bit of detail too but I really have to go and get some work done. :slight_smile:

3 Likes

Hahahaha… I am reading all this with a big smile!.. Thanks for the additions and insights… and comments… :smiley: I love this kind of " agile " iterations.

6 Likes

Brillant! Thanks for your sharing of such a pure script project

1 Like