How to resize the player's ship?

Hi, I’m using Godot 4.3 and I’m updating my shootem up but I’m having a problem. I’d like to resize the elements on the screen. I’m testing my ship first but I’m not sure how to resize it all while keeping its base speed and position and limits so that it doesn’t go out of the screen. I’ve tested several scripts but without much success. When I resize the screen manually the ship changes position in x.


Test code for ship resizing.

extends Area2D

@export var speed: float = 300.0
@export var laser_scene: PackedScene
@export var laser_sound: AudioStream = preload("res://musiqueetson/amiral3000lasersound.wav")
@export var boom_texture: Texture = preload("res://alienminigames/boomtexture.png")

var can_fire: bool = true
var controls_enabled: bool = true   # Contrôle local pour le traitement (mais on utilisera aussi le flag "frozen")
var frozen: bool = false            # Nouvel indicateur pour forcer l'arrêt complet du traitement
var initial_y: float = 0.0          # Position Y initiale

# Variables pour la gestion du redimensionnement
var base_resolution := Vector2(1280, 720)  # Résolution d'origine
var base_scale := Vector2(1.0, 1.0)        # Échelle d'origine du sprite
var screen_size := Vector2.ZERO            # Taille actuelle de l'écran
var left_boundary: float = 60              # Limite gauche à la résolution de base
var right_boundary: float = 1220           # Limite droite à la résolution de base

# Gestion des vies
var player_lives: int = 3
var life_sprites: Array = []

# Variables d'explosion
var is_exploding: bool = false
var original_texture: Texture      # Pour sauvegarder le sprite normal
var original_scale: Vector2        # Pour sauvegarder l'échelle d'origine

func _ready() -> void:
	# Tenter d'utiliser la méthode générique pour définir le mode pause (valeur 1 = STOP)
	set("pause_mode", 1)
	
	initial_y = position.y
	if not laser_scene:
		laser_scene = preload("res://lasertest.tscn")
	
	var level_scene = get_tree().current_scene
	life_sprites = [
		level_scene.get_node("vievaisseau1"),
		level_scene.get_node("vievaisseau2"),
		level_scene.get_node("vievaisseau3")
	]
	
	original_texture = $Sprite2D.texture
	original_scale = $Sprite2D.scale
	base_scale = Vector2($Sprite2D.scale.x, $Sprite2D.scale.y)  # Mémoriser l'échelle de base

	connect("area_entered", Callable(self, "_on_area_entered"))
	
	# Enregistrer la taille initiale de l'écran
	screen_size = get_viewport_rect().size
	
	# Se connecter au signal de redimensionnement de la fenêtre
	get_tree().root.connect("size_changed", Callable(self, "_on_window_resize"))
	
	# Adapter le vaisseau à la résolution initiale
	adjust_to_screen_size()
	
	print("Vaisseau prêt. Contrôles actifs =", controls_enabled, ", frozen =", frozen)

func _physics_process(delta: float) -> void:
	# Si le vaisseau est figé, on ne fait rien.
	if frozen:
		return

	if not is_exploding and controls_enabled:
		var velocity: Vector2 = Vector2.ZERO
		if Input.is_action_pressed("ui_left"):
			velocity.x -= 1
		if Input.is_action_pressed("ui_right"):
			velocity.x += 1
		if velocity.length() > 0:
			# Adapter la vitesse à la résolution actuelle
			var adjusted_speed = speed * (screen_size.x / base_resolution.x)
			velocity = velocity.normalized() * adjusted_speed
		position.x += velocity.x * delta
		
		# Adapter les limites à la résolution actuelle
		var scaled_left = left_boundary * (screen_size.x / base_resolution.x)
		var scaled_right = right_boundary * (screen_size.x / base_resolution.x)
		position.x = clamp(position.x, scaled_left, scaled_right)
		
		# Maintenir la position Y fixe en bas de l'écran
		position.y = initial_y
		
		if Input.is_action_just_pressed("fire") and can_fire and controls_enabled:
			fire_laser()
	else:
		# Débogage pour vérifier l'état
		print("Vaisseau inactif: is_exploding =", is_exploding, ", controls_enabled =", controls_enabled)

func _on_window_resize() -> void:
	screen_size = get_viewport_rect().size
	adjust_to_screen_size()

func adjust_to_screen_size() -> void:
	# Calculer le ratio par rapport à la résolution de base
	var scale_ratio_x = screen_size.x / base_resolution.x
	var scale_ratio_y = screen_size.y / base_resolution.y
	
	# Appliquer l'échelle au sprite
	$Sprite2D.scale = base_scale * min(scale_ratio_x, scale_ratio_y)
	
	# IMPORTANT: Forcer la position Y en bas de l'écran (avec un décalage fixe)
	# On suppose que le vaisseau est à ~90% de la hauteur de l'écran dans la conception originale
	position.y = screen_size.y * 0.9
	initial_y = position.y
	
	# Redimensionner la collision si nécessaire
	if has_node("CollisionShape2D"):
		$CollisionShape2D.scale = Vector2(scale_ratio_x, scale_ratio_y)
	
	# Ajuster la position du point d'émission des lasers
	if has_node("Projectile"):
		# Repositionner le point d'émission des lasers proportionnellement
		var original_offset = $Projectile.position.y / base_resolution.y
		$Projectile.position.y = original_offset * screen_size.y

func fire_laser() -> void:
	var laser_instance = laser_scene.instantiate()
	laser_instance.global_position = $Projectile.global_position
	
	# Ajuster l'échelle du laser en fonction de la résolution
	var scale_ratio = min(screen_size.x / base_resolution.x, screen_size.y / base_resolution.y)
	if laser_instance.has_node("Sprite2D"):
		laser_instance.get_node("Sprite2D").scale *= scale_ratio
	if laser_instance.has_node("CollisionShape2D"):
		laser_instance.get_node("CollisionShape2D").scale *= scale_ratio
	
	get_tree().current_scene.add_child(laser_instance)
	laser_instance.add_to_group("player_laser")
	can_fire = false
	laser_instance.connect("laser_removed", Callable(self, "_on_laser_removed"))
	
	if laser_sound:
		$AudioStreamPlayer.stream = laser_sound
		print("Lecture du son du laser")
		$AudioStreamPlayer.play()
	else:
		print("Aucun son de laser assigné!")

func _on_laser_removed() -> void:
	can_fire = true

func _on_area_entered(area: Area2D) -> void:
	if area.is_in_group("enemy_laser"):
		if not is_exploding:
			take_damage()
		area.queue_free()

func take_damage() -> void:
	if is_exploding:
		return
	if player_lives > 0:
		player_lives -= 1
		life_sprites[player_lives].hide()
		print("Joueur touché ! Vies restantes :", player_lives)
		start_explosion()
	else:
		die()

func start_explosion() -> void:
	is_exploding = true
	$Sprite2D.texture = boom_texture
	$Sprite2D.scale = original_scale * 0.7 * (screen_size.x / base_resolution.x)
	$ExplosionPlayer.play()
	if not $ExplosionPlayer.is_connected("finished", Callable(self, "_on_explosion_finished")):
		$ExplosionPlayer.connect("finished", Callable(self, "_on_explosion_finished"))

func _on_explosion_finished() -> void:
	if player_lives == 0:
		die()
	else:
		$Sprite2D.texture = original_texture
		# Appliquer l'échelle adaptée à la résolution actuelle
		var scale_ratio = min(screen_size.x / base_resolution.x, screen_size.y / base_resolution.y)
		$Sprite2D.scale = base_scale * scale_ratio
		is_exploding = false

func die() -> void:
	get_tree().call_deferred("change_scene_to_file", "res://gameover.tscn")

# Fonction destinée à désactiver les contrôles du vaisseau.
# Cette fonction doit être appelée par le script de fin de niveau.
func disable_controls() -> void:
	print("disable_controls() appelée - le vaisseau doit être figé.")
	controls_enabled = false
	can_fire = false
	frozen = true   # On active le flag pour stopper complètement _physics_process
	# On désactive également les traitements de ce node (bien que le flag devrait suffire)
	set_process(false)
	set_physics_process(false)
	set_process_input(false)
	disable_all_lasers()

func disable_all_lasers() -> void:
	for laser in get_tree().get_nodes_in_group("player_laser"):
		laser.queue_free()
	for laser in get_tree().get_nodes_in_group("enemy_laser"):
		laser.queue_free()

The original code of the player’s ship without resizing

extends Area2D

@export var speed: float = 300.0
@export var laser_scene: PackedScene
@export var laser_sound: AudioStream = preload("res://musiqueetson/amiral3000lasersound.wav")
@export var boom_texture: Texture = preload("res://alienminigames/boomtexture.png")

var can_fire: bool = true
var controls_enabled: bool = true   # Contrôle local pour le traitement (mais on utilisera aussi le flag "frozen")
var frozen: bool = false            # Nouvel indicateur pour forcer l'arrêt complet du traitement
var initial_y: float = 0.0          # Position Y initiale

# Gestion des vies
var player_lives: int = 3
var life_sprites: Array = []

# Variables d'explosion
var is_exploding: bool = false
var original_texture: Texture      # Pour sauvegarder le sprite normal
var original_scale: Vector2        # Pour sauvegarder l'échelle d'origine

func _ready() -> void:
	# Tenter d'utiliser la méthode générique pour définir le mode pause (valeur 1 = STOP)
	set("pause_mode", 1)
	
	initial_y = position.y
	if not laser_scene:
		laser_scene = preload("res://lasertest.tscn")
	
	var level_scene = get_tree().current_scene
	life_sprites = [
		level_scene.get_node("vievaisseau1"),
		level_scene.get_node("vievaisseau2"),
		level_scene.get_node("vievaisseau3")
	]
	
	original_texture = $Sprite2D.texture
	original_scale = $Sprite2D.scale

	connect("area_entered", Callable(self, "_on_area_entered"))
	
	print("Vaisseau prêt. Contrôles actifs =", controls_enabled, ", frozen =", frozen)

func _physics_process(delta: float) -> void:
	# Si le vaisseau est figé, on ne fait rien.
	if frozen:
		# Pour debug, on affiche une fois
		# print("Vaisseau figé (frozen = true)")
		return

	if not is_exploding and controls_enabled:
		var velocity: Vector2 = Vector2.ZERO
		if Input.is_action_pressed("ui_left"):
			velocity.x -= 1
		if Input.is_action_pressed("ui_right"):
			velocity.x += 1
		if velocity.length() > 0:
			velocity = velocity.normalized() * speed
		position.x += velocity.x * delta
		position.x = clamp(position.x, 60, 1220)
		position.y = initial_y
		if Input.is_action_just_pressed("fire") and can_fire and controls_enabled:
			fire_laser()
	else:
		# Débogage pour vérifier l'état
		print("Vaisseau inactif: is_exploding =", is_exploding, ", controls_enabled =", controls_enabled)

func fire_laser() -> void:
	var laser_instance = laser_scene.instantiate()
	laser_instance.global_position = $Projectile.global_position
	get_tree().current_scene.add_child(laser_instance)
	laser_instance.add_to_group("player_laser")
	can_fire = false
	laser_instance.connect("laser_removed", Callable(self, "_on_laser_removed"))
	
	if laser_sound:
		$AudioStreamPlayer.stream = laser_sound
		print("Lecture du son du laser")
		$AudioStreamPlayer.play()
	else:
		print("Aucun son de laser assigné!")

func _on_laser_removed() -> void:
	can_fire = true

func _on_area_entered(area: Area2D) -> void:
	if area.is_in_group("enemy_laser"):
		if not is_exploding:
			take_damage()
		area.queue_free()

func take_damage() -> void:
	if is_exploding:
		return
	if player_lives > 0:
		player_lives -= 1
		life_sprites[player_lives].hide()
		print("Joueur touché ! Vies restantes :", player_lives)
		start_explosion()
	else:
		die()

func start_explosion() -> void:
	is_exploding = true
	$Sprite2D.texture = boom_texture
	$Sprite2D.scale = original_scale * 0.7
	$ExplosionPlayer.play()
	if not $ExplosionPlayer.is_connected("finished", Callable(self, "_on_explosion_finished")):
		$ExplosionPlayer.connect("finished", Callable(self, "_on_explosion_finished"))

func _on_explosion_finished() -> void:
	if player_lives == 0:
		die()
	else:
		$Sprite2D.texture = original_texture
		$Sprite2D.scale = original_scale
		is_exploding = false

func die() -> void:
	get_tree().call_deferred("change_scene_to_file", "res://gameover.tscn")

# Fonction destinée à désactiver les contrôles du vaisseau.
# Cette fonction doit être appelée par le script de fin de niveau.
func disable_controls() -> void:
	print("disable_controls() appelée - le vaisseau doit être figé.")
	controls_enabled = false
	can_fire = false
	frozen = true   # On active le flag pour stopper complètement _physics_process
	# On désactive également les traitements de ce node (bien que le flag devrait suffire)
	set_process(false)
	set_physics_process(false)
	set_process_input(false)
	disable_all_lasers()

func disable_all_lasers() -> void:
	for laser in get_tree().get_nodes_in_group("player_laser"):
		laser.queue_free()
	for laser in get_tree().get_nodes_in_group("enemy_laser"):
		laser.queue_free()

Basically, it sounds like you want to define how window stretching affect the game scene.

Head over to the editor’s Project Settings under the Project tab, and find Display -> Window in the left menu, where you can see a Stretch section contains a Mode setting.

We usually express it like this: find the UI Project -> Project Settings, then find the setting display/window/stretch/mode.

I’ve already tried with the stretch settings but there are still problems. The limits of the player’s ship and the enemy formation are no longer the same when I enlarge and reduce the screen and it gives a strange result when I decrease the resolution horizontally. Also the Sprite2D of the background shrinks when I decrease the screen resolution and does not take all the screen resolution when the game window is at its normal resolution (1280X720) or when I enlarge the screen in a higher resolution.

You’ll be better without your own resize code, leave it to the Camera and window stretching settings. Try using the original code and play around with the settings, and add a Camera if you need the view anchored to the center.

I used a Camera2D and set Camera2D to center and now all objects are centered but on the other hand there is a huge gap between the play area. I would like it to take up the whole screen.

For things fixed to the screen rather than the world, you can use CanvasLayer with Control nodes, which are used to make UI. Make a TextureRect under a CanvasLayer and decrease its layer so it won’t cover everything.

I tried the method in an example scene with Controls and with the CanvasLayer but when I resize to this dimension the image is centered in the middle but does not distort to take the entire width of the game window.

Instead of sizing the TextureRect, you should set its anchors. There’s a “Full rect” preset that sets the anchors to the screen corners.

Learn about UI and Control nodes.

By the way, your project seems somewhat tough and lost. The best way to learn is to do small projects through tutorials, make mistakes, gain experience, until you can finally do it yourself.

I tried to reproduce your example and it works now. I didn’t know that you could resize with Anchor without needing to configure a GDScript code for resizing. I will try with my game and see if it works.

You must first select the TextureRect node in the editor, and the anchors preset button will be shown in the scene view top bar. My advice is still the same: take a full tutorial of Godot, like the Brackeys’, just like you need to take a lesson to learn it, before doing it yourself.

On the other hand, I hadn’t thought about Control nodes when I created my game. I have to rethink everything. How do you put an Anchor on a Sprite2D if you want it to be centered?

Here is the scene from the level that is problematic with all its nodes.

The Sprite2D is a Node2D node, not Control, only Control nodes have anchors. Use TextureRect instead of Sprite2D

Once again, you didn’t understand the fundamentals of the node system; taking some tutorials will help you understand the nodes. Don’t get stressed, tutorials are fun, and you will grasp a lot after a few days.

Before, I had no experience with coding, but I still played around. It was interesting but stressful, for I was fumbling about. I didn’t know how to achieve my goal. After reading books and watching videos, I finally understood the functions, classes, and how to design my code structure nicely, and it felt easy to solve problems.

Finally, I solved the problem. Just go to Project Settings - General - Display - Window - Stretch.

Mode: Viewport
Aspect: Ignore

Next, you need to add a Camera2D to the scene and set it to Fixed Top Left or Drag Center (depending on how the scene is configured).

Then add this script to the Node2D of the main scene. It will simply upscale all the elements in the scene. This way, it upscales all the elements in the scene without needing to configure resizing for each element.

extends Node2D

# La résolution de référence (votre résolution virtuelle)
@export var base_resolution: Vector2 = Vector2(1280, 720)

func _ready() -> void:
	# On force le redimensionnement dès le démarrage et à chaque changement de taille du viewport.
	update_scale()
	get_viewport().connect("size_changed", Callable(self, "update_scale"))

func update_scale() -> void:
	var vp_size: Vector2 = get_viewport_rect().size
	# Calcul différent pour X et Y afin de remplir exactement le viewport
	var scale_x: float = vp_size.x / base_resolution.x
	var scale_y: float = vp_size.y / base_resolution.y
	self.scale = Vector2(scale_x, scale_y)

To be honest, you didn’t have to do that. The Viewport.size_changed will only emit when the stretch mode is disabled. What effect do you actually need?

I just needed to resize the scene and all elements for all screen types.

Maybe I should ask like this:

  1. Do you want to scale the scene proportional to the window’s expansion, or just letting it with the initial scale?
  2. Do you want to keep the player fixed to the bottom of the window instead of top?

I already explained to you in a previous reply that I solved my problem. However, thank you for enlightening me on the viewport method and window stretching in Godot.

Now the level elements are resized proportionally to the window enlargement, and the player remains at the bottom of the window.

I added GDScript code to upscale the game window so that it fully adapts to the screen resolution and allows elements to be dynamically resized. However, the only downside is that it can distort the elements, especially on devices with long rectangle resolutions, but that doesn’t bother me.

Here’s a simple video so you understand what I wanted.

To sum up, no matter what, at least you can be sure that you want the scale proportional to the window size. Stick with the canvas_items stretch mode.

Now with the stretching setup, let’s talk about the aspect ratio. First, get rid of all the resizing code, then:

  1. If you don’t care about the distortion, you can set the aspect with ignore
    image

  2. However, the distortion should still be a trouble to you. Here’s what you can do.

    • Aspect = keep. Covers up areas outside the boundaries.
    • Aspect = keep_width. Allows expanded height, and covers up the width when bigger than the initial aspect ratio.
    • Aspect = keep_height. Allows expanded width, and covers up the height when bigger than the initial aspect ratio.
    • Aspect = expand. Allows either expanded width or height.

With keep_width, keep_height, and expand, extra spaces beyond the original size are included in the view. Therefore, we must consider your requirement that the player be fixed to the bottom of the camera’s view. In other words, when the camera’s view expands, the extra included view should be from the top, not the bottom.

Here’s a trick to align the camera’s x to the center, and set its bottom to be fixed to the world origin. Keep all your camera’s properties as usual, but set the bottom limit to 0, which keeps its view’s bottom edge above y=0. Thus, the extra view width will expand from both sides.

I suggest you read