Make Eternal Loom animation dynamic based on threads collected by the player - #2864
Conversation
…-1030 merging changes from endless main branch
|
Test build no longer available. |
|
Short video fo how it should look loom_animation_test.mp4 |
There was a problem hiding this comment.
This is impressive work, already. Thanks! Here are some requests and suggestions.
The Path2D you have is made of straight lines:
I think it would be nice to make it a circle. The curve of a Path2D is a Bezier curve so this is possible:
However, if following this advice and the one for sampling the points, you will have to add a PathFollow2D node and tween the progress, for positioning each thread.
| const WORLD_MEMORY = preload("uid://5wscjc8yqqts") | ||
| const WORLD_SPIRIT = preload("uid://cepg1o3ihp055") | ||
|
|
||
| @export var _play_animation: bool = false: |
There was a problem hiding this comment.
Nice, I like how you made it very easy to test the animation from the editor with this. But please check the warning on this line. It says that the variable is not used. There is a better way: Adding an @export_tool_button, which adds a button to the Inspector. There are a number of examples in this repository already. For example, the Randomize button in townie.tscn
| # Time between each animation_point | ||
| var timer: float = animation_time / (animation_points.size() * 2.8) | ||
| # Separation between threads | ||
| var separator: int = animation_points.size() / floor(thread_list.size()) |
There was a problem hiding this comment.
Since #1234 was merged, zero threads is a possible number. And the script will raise a zero division error. So you may want to skip this animation entirely for zero threads.
| var arr: Array[InventoryItem] | ||
| for i in range(debug_thread_count): | ||
| var temp_item := InventoryItem.new() | ||
| temp_item.type = InventoryItem.ItemType.MEMORY |
There was a problem hiding this comment.
It would be nice to pick the item type randomly for better debugging.
|
|
||
|
|
||
| func _loom_animation_play(thread_list: Array[InventoryItem]) -> void: | ||
| var animation_points := animation_path.curve.get_baked_points() |
There was a problem hiding this comment.
Since you made it so easy to test (thanks!) I tried with 20 threads and saw a single one being animated. I see now that this works with up to 12 threads: because the curve of your Path2D node has 12 points.
Making it work with more than 12 threads is not necessary, but I think there is a better way: you can pick points from the sampled curve:
var animation_points: Array[Vector2]
var curve_length := animation_path.curve.get_baked_length()
var separation := curve_length / thread_list.size()
for i in thread_list.size():
var offset := separation * i
var point := animation_path.curve.sample_baked(offset)
animation_points.append(point)
Then it will work with 20+ threads!
Grabacion.de.pantalla.desde.2026-09-09.14-20-22.mp4
| match thread.type: | ||
| InventoryItem.ItemType.MEMORY: | ||
| sprite.texture = WORLD_MEMORY | ||
| InventoryItem.ItemType.IMAGINATION: | ||
| sprite.texture = WORLD_IMAGINATION | ||
| InventoryItem.ItemType.SPIRIT: | ||
| sprite.texture = WORLD_SPIRIT | ||
| _: | ||
| sprite.texture = WORLD_SPIRIT |
There was a problem hiding this comment.
Not needed! There is a helper function already in inventory_item. You can just use:
sprite.texture = thread.get_world_texture()
Took this into consideration and used the PathFollow2D to move every thread along the circumference Should look like this loom_test_2.mp4 |
manuq
left a comment
There was a problem hiding this comment.
This is impressive! Great work! Approving, please consider my suggestions. They are tiny details, but overal the new dynamic animation is fantastic.
| var arr: Array[InventoryItem] | ||
| for i in range(debug_thread_count): | ||
| var temp_item := InventoryItem.new() | ||
| var item_types := InventoryItem.ItemType.values() | ||
| item_types.remove_at(3) ## Remove the 'None' Item Type | ||
| temp_item.type = item_types.pick_random() | ||
| arr.append(temp_item) | ||
| _loom_animation_play(arr) |
There was a problem hiding this comment.
Is better to use descriptive variable names. Also item types will be the same on each for loop, so it should be outside. And the way you are removing ItemType.NONE is by index, which will fail as soon as that enum changes, so is not the best. I suggest for now using InventoryItem.COLORS_PER_TYPE.keys() or an explicit array. In the future we can consider adding a utility function.
| var arr: Array[InventoryItem] | |
| for i in range(debug_thread_count): | |
| var temp_item := InventoryItem.new() | |
| var item_types := InventoryItem.ItemType.values() | |
| item_types.remove_at(3) ## Remove the 'None' Item Type | |
| temp_item.type = item_types.pick_random() | |
| arr.append(temp_item) | |
| _loom_animation_play(arr) | |
| var debug_items: Array[InventoryItem] | |
| # TODO: InventoryItem needs a utility function to obtain all types except NONE. | |
| var item_types := InventoryItem.COLORS_PER_TYPE.keys() | |
| for i in range(debug_thread_count): | |
| var item := InventoryItem.new() | |
| item.type = item_types.pick_random() | |
| debug_items.append(item) | |
| _loom_animation_play(debug_items) |
|
|
||
| var counter: int = 0 | ||
| for thread in thread_list: | ||
| var path_follow := PathFollow2D.new() |
There was a problem hiding this comment.
Excellent! Yes I see you need one PathFollow2D per thread.
|
|
||
| animation_path.add_child(path_follow) | ||
|
|
||
| ## Starting point for each thread |
There was a problem hiding this comment.
This is not a documentation comment, so it should have a single #.
| ## Starting point for each thread | |
| # Starting point for each thread |
| ## Starting point for each thread | ||
| path_follow.progress_ratio = counter * separation | ||
|
|
||
| ## Time variation between threads |
There was a problem hiding this comment.
| ## Time variation between threads | |
| # Time variation between threads |
|
|
||
| var tween := path_follow.create_tween() | ||
|
|
||
| ## The first thread is the last to end |
There was a problem hiding this comment.
| ## The first thread is the last to end | |
| # The first thread is the last to end |
| counter = counter + 1 | ||
|
|
||
|
|
||
| ## Remove thread once animation finishes |
There was a problem hiding this comment.
| ## Remove thread once animation finishes | |
| # Remove thread once animation finishes |
| tween.parallel().tween_property(sprite, "modulate", Color(0, 0, 0, 0), 0.5) | ||
| tween.parallel().tween_property(sprite, "scale", Vector2(3, 3), 0.5) | ||
| tween.tween_callback(_thread_free.bind(sprite)) | ||
| counter = counter + 1 |
There was a problem hiding this comment.
We tend to have this shorter version everywhere else:
| counter = counter + 1 | |
| counter += 1 |
| tween.tween_property(sprite, "scale", Vector2(1, 1), 0.5) | ||
| ## Loop around | ||
| tween.parallel().tween_property( | ||
| path_follow, "progress_ratio", 2.0, loop_time - time_variation * counter |
There was a problem hiding this comment.
I see, so when counter is zero this property is tweened by loop_time seconds. That's why the first thread in the for loop is the one emitting animation_finished.
| position = Vector2(0, 39) | ||
| position = Vector2(-1, 39) |
There was a problem hiding this comment.
This change is unrelated, please revert it.
| position = Vector2(0, 135) | ||
| position = Vector2(0, 85) | ||
| script = ExtResource("6_rle0d") | ||
| debug_thread_count = 7 |
There was a problem hiding this comment.
Maybe commit the tscn file with the default amount for debug_thread_count.
Adds an script that replaces loom_offering_animation.
This new version dynamically shows the number of threads the player collected in the current quest with it's corresponding color
Uses a Path2D to move the threads
Resolves: #1861