[2D] Create a customizable Platform scene

Godot Version

4.3

Question

Hi! I’m creating a simple 2D platformer.
For the map, I used a TileMap (actually a tilemap layer because of Godot 4.3) where I “painted” the tiles to handle collisions.

Now I want to create platforms.
I have a tiled image for the platform (center piece + the two edges).
So, for static platforms I used the tile map directly; in this way, I can choose the length of the platform.

But I also want to create moving platforms. Here I have some problems with the sprites.
I would like to create a reusable scene for the platform, where you can choose the length (1,2,3 …) instead of creating a different scene for every length (and possibly select the sprite). Is it possible?

If not and I have to create a different scene for each platform, how can I use the tiled sprite? Do I need to use “n” Sprite2D in the platform scene or do I need to create an ad hoc sprite?

Thanks!

I think you can use a NinePatchRect to achieve what you want. Is your platform a rigidbody2D ?

Thanks for the suggestion!
I’m using an AnimatableBody2D for the platform.

Using the NinePatchRect I was able to put the platform in a scene and adjust its size, then through code, I set the size of the collider with the one of the NinePathRect.

I had a problem in making it move.
If I move the position of the NinePatchRect visually everything is ok. The collider seems to move with the sprite (I enabled “Visible Collision Shapes”), but the actual collider is not moved but remains at its original position. So, I ended up moving both in the script.
Another thing is that the sprite seems to lag a little bit while moving.

Now I don’t know if this is the best approach.

So this is the platform:
moving_platform_scene_image

And this is the code:

Code
extends NinePatchRect

@export var start_position: Vector2 # TODO rethink export
@export var end_position: Vector2
@export var SPEED: float = 80

@onready var collision_shape_2d: CollisionShape2D = $AnimatableBody2D/CollisionShape2D
@onready var animatable_body_2d: AnimatableBody2D = $AnimatableBody2D
# The position to which the platform is moving
var target_position: Vector2;

func _ready() -> void:
	# Match the size of the collider with the size of the sprite
	var rect: RectangleShape2D = collision_shape_2d.shape
	rect.size.x = size.x - 4
	rect.size.y = size.y
	collision_shape_2d.position.x = size.x/2
	collision_shape_2d.position.y = size.y/2
	# Init moving points
	start_position = position
	target_position = end_position

func _process(delta: float) -> void:
	# If the platform reaches the target position, change it
	if position.is_equal_approx(target_position):
		target_position = start_position if target_position == end_position else end_position
	# Move the NinePatchRect
	position = lerp(position, position.move_toward(target_position, delta  * 80), 0.5)
	# For some reason the animatable body must be moved separately ???
	animatable_body_2d.global_position = position

First think i see, you need to puts your update in the physics process as it change the collider position. I continue to read your code.

Its correct in my opinion because NinePatchRect is a Control and not a game node but it’s weird that collision preview move correctly and not actual physics object. But your code work fine for me.

Yes, the debug view is really strange, I did a video of it:

Video

Thanks for the feedback on the code.
It was already in the physics_process then I changed it to see if there was some difference and forgot to change it back!

The only problem is the sprite of the platform that is jittering, maybe you have some idea why

Video

To be honest, I don’t know why. It’s almost imperceptible in your video (compression I guess) but I understand what you mean.

Maybe by activating the Snap 2D transform option.
In the project settings : rendering/2d/snap/snap_2d_transforms_to_pixel

Unluckily it didn’t work.

The only way I found to have a smooth movement is by using the animator and setting the start and end position through code, but I can’t keep the sprite and the collider in sync in this way…

I think I will take the easiest path and create multiple platform scenes!

Thanks anyway, I marked the first reply as the answer!

I have try something, but don’t know if it can work. I recreate the behaviour of the ninepatch for a sprite node (this way it’s not a control node).
Here is the script :

SpritePlateform
@tool
class_name Plateform
extends Sprite2D

# Plateform width in pixel
@export var plateform_width : int = 60:
	get:
		return plateform_width
	set(value):
		plateform_width = value
		_refresh_texture()
# Movement start from editor position to this position
@export var move_to: Vector2
# Speed of the plateform
@export var speed: float = 80
@export_category("Texture")
# Texture use to generate the plateform texture
@export var plateform_texture: Texture2D :
	get:
		return plateform_texture
	set(value):
		plateform_texture = value
		_refresh_texture()
# Type of stretch (see ninepatch for more info)
@export_enum("Stretch", "Tile", "Tile Fit") var stretch_mode : int = 0:
	get:
		return stretch_mode
	set(value):
		stretch_mode = value
		_refresh_texture()
# Only left and right margin is used and same way as ninepatch
@export_group("Margin")
@export var left : int = 0:
	get:
		return left
	set(value):
		left = value
		_refresh_texture()
@export var right : int = 0:
	get:
		return right
	set(value):
		right = value
		_refresh_texture()

# Recreate texture when parameters changes
func _refresh_texture():
	# If no texture is defined return
	if plateform_texture == null: return

	var base_texture = plateform_texture.get_image()
	var base_size = plateform_texture.get_size()
	var base_format = base_texture.get_format()

	# Create a new image with good width (cannot be under left+right margin width)
	var plateforme_image = Image.create(max(plateform_width, left + right), base_size.y, false, base_format)

	# Add left and right part
	plateforme_image.blit_rect(base_texture, Rect2i(0, 0, left, base_size.y), Vector2i.ZERO)
	plateforme_image.blit_rect(base_texture, Rect2i(base_size.x - right, 0, right, base_size.y), Vector2i(plateform_width - right, 0))

	# If size inbetween is to small, stop here
	if (plateform_width - left - right <= 0):
		texture = ImageTexture.create_from_image(plateforme_image)
		return
	
	# Get filter for stretch and tile fit mode
	var filter = ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter")
	# Get middle part of the texture
	var part_middle = Image.create(base_size.x - left - right, base_size.y, false, base_format)
	part_middle.blit_rect(base_texture, Rect2i(left, 0, plateform_width - left - right, base_size.y), Vector2i.ZERO)

	var w = plateform_width - left - right;
	var h = base_size.y
	if (stretch_mode == 0): # Stretch mode

		part_middle.resize(w, h, filter)
		plateforme_image.blit_rect(part_middle, Rect2i(0, 0, w, h), Vector2i(left, 0))

	elif (stretch_mode == 1): # Tile mode

		var x = 0;
		var dx = base_size.x - left - right
		while x <= w:
			plateforme_image.blit_rect(part_middle, Rect2i(0, 0, min(dx, w - x), h), Vector2i(left + x, 0))
			x += dx;
		
	elif (stretch_mode == 2): # Tile fit mode

		var dx = base_size.x - left - right
		var part_middle2 = Image.create(floor(w / dx) * dx, base_size.y, false, base_format)
		for i in range(floor(w / dx)):
			part_middle2.blit_rect(part_middle, Rect2i(0, 0, dx, h), Vector2i(i * dx, 0))
		part_middle2.resize(w, h, filter)
		plateforme_image.blit_rect(part_middle2, Rect2i(0, 0, w, h), Vector2i(left, 0))

	# Assign texture to the sprite2D texture
	texture = ImageTexture.create_from_image(plateforme_image)




@onready var collision_shape_2d: CollisionShape2D = $AnimatableBody2D/CollisionShape2D
@onready var animatable_body_2d: AnimatableBody2D = $AnimatableBody2D
@onready var start_position = position
# The position to which the platform is moving
var target_position: Vector2;

func _ready() -> void:
	# Match the size of the collider with the size of the sprite
	var rect: RectangleShape2D = collision_shape_2d.shape
	var base_size = texture.get_size()
	rect.size.x = base_size.x - 4
	rect.size.y = base_size.y
	# Init moving points
	target_position = move_to

func _physics_process(delta: float) -> void:
	if not Engine.is_editor_hint():
		# If the platform reaches the target position, change it
		if position.is_equal_approx(target_position):
			target_position = start_position if target_position == move_to else move_to
		# Move the NinePatchRect
		position = lerp(position, position.move_toward(target_position, delta  * 80), 0.5)
		# For some reason the animatable body must be moved separately ???
		animatable_body_2d.global_position = position

It’s a simplified version (its not a real ninepatch anymore, it can only expend left and right). Attach it to a Sprite2D and set your plateforme texture in the “plateform texture” property living the sprite2D original texture blank. The script will generate the texture based on the size of the plateform.
Hope this can get rid of jittering

That was a lot of work, thank you!

I tried it, but it seems (probably I’m misusing it) difficult to use. However, I learned a lot of cool things!

The NinePatchRect was the root because its size was resizable from the editor after you imported the scene into your level.
In the end, I put the AnimatableBody2D as the root node and made the script move it, while to modify the NinaPatchRect I just imported the Platform in the level scene and then modified its children nodes.

Thanks again for all the support!

In case you want to give it a try here is a small explanation :

You need a configuration like your one :

Tree

Image 2

And set the parameters like so :

Inspector

Hi! Sorry for the late reply!

I was too quick in the previous reply. The usage was simple, with difficult I meant that the vertical size is determined by the sprite used and that if you change the size from the editor it will break the automatic computation (even if I was not able to understand why, because the script should adjust it anyway).

Also, the “fit” mode doesn’t work properly (again, the code seems correct; but we don’t care in this case cause the “tiled” mode is better for things that can change length :smiley: ).

Anyway, I find the solution of just swapping the root element and modifying the NinePatchRect child manually more flexible, even if it seems like a workaround.

Thanks for your help!