How to start & travel in AnimationTree using code? - godot

I'm trying to animate an expandable staff by travel between nodes in AnimationTree Like this:
...
tool
export(String,"small", "mid", "full") var staff_mode = "small" setget set_staff_mode;
func set_staff_mode(new_val):
var ani_state;
if(self.has_node("/path/to/AnimationTree")):
ani_state=self.get_node("/path/to/AnimationTree")["parameters/playback"];
ani_state.start(staff_mode);
print(ani_state.is_playing());
ani_state.travel(new_val);
ani_state.stop();
staff_mode=new_val;
I haven't applied autoplay to small because I don't want a looping animation on the staff
(it only expands or compresses, no idle animation)
but for some reason it gives the error:
Can't travel to 'full' if state machine is not playing. Maybe you
need to enable Autoplay on Load for one of the nodes in your state
machine or call .start() first?
Edit:
I forgot to mention but I don't have any idle animation for my staff so I need to stop the animation after the transition is complete.
small, mid & full
(all of them are static modes of the staff depending upon the game how much the staff should extend)
are all 0.1sec single frame animations and I applied Xfade Time of 0.2 secs to show the transition
I simply need to transition from an existing animation state to another and then stop

New Answer
Apparently the solution on the old answer does not work for very short animations. And the workarounds begin to seem to much overhead for my taste. So, as alternative, let us get rid of the AnimationTree and work directly with AnimationPlayer. To do this, we will:
Increase the animation duration to be long enough for the "cross fade" time (e.g. 0.2 seconds).
Put the "cross fade" time in "Cross-Animation Blend Times". For each animation, select it on the Animation panel and then select "Edit Transition…" from the animation menu, that opens the "Cross-Animation Blend Times" where you can specify the transition time to the other animations (e.g. 0 to itself, 0.1 to "adjacent" animation, and so on).
Now we can simply ask the AnimationPlayer to play, something like this:
tool
extends Node2D
enum Modes {full, mid, small}
export(Modes) var staff_mode setget set_staff_mode
func set_staff_mode(new_val:int) -> void:
if staff_mode == new_val:
return
if not is_inside_tree():
return
var animation_player := get_node("AnimationPlayer") as AnimationPlayer
if not is_instance_valid(animation_player):
return
var target_animation:String = Modes.keys()[new_val]
animation_player.play(target_animation)
yield(animation_player, "animation_finished")
staff_mode = new_val
property_list_changed_notify()
I have opted to use an enum, because this will also allow me to "travel" between the animations. The idea is that we will make a for loop where we call the animations in order. Like this:
tool
extends Node2D
enum Modes {full, mid, small}
export(Modes) var staff_mode:int setget set_staff_mode
func set_staff_mode(new_val:int) -> void:
if staff_mode == new_val:
return
if not is_inside_tree():
return
var animation_player := get_node("AnimationPlayer") as AnimationPlayer
if not is_instance_valid(animation_player):
return
var old_val := staff_mode
staff_mode = new_val
var travel_direction = sign(new_val - old_val)
for mode in range(old_val, new_val + travel_direction, travel_direction):
var target_animation:String = Modes.keys()[mode]
animation_player.play(target_animation)
yield(animation_player, "animation_finished")
I have also decided to set staff_mode early so I can avoid property_list_changed_notify.
Concurrent calls may result in animation stopping early, since calling play stops the currently playing animation to play the new one. However, I don't think waiting for the current animation to end is correct. Also with so short animations, it should not be a problem.
Version using Tween
Using Tween will give you finer control, but it is also more work, because we are going to encode the animations in code… Which I will be doing with interpolate_property. Thankfully this is a fairly simple animation, so can manage without making the code too long.
Of course, you need to add a Tween node. We will not use AnimationPlayer nor AnimationTree. Tween will handle the interpolations (and you can even specify how to do the interpolations by adding the optional parameters of interpolate_property, which I'm not passing here).
This is the code:
tool
extends Node2D
enum Modes {full, mid, small}
export(Modes) var staff_mode:int setget set_staff_mode
func set_staff_mode(new_val:int) -> void:
if staff_mode == new_val:
return
if not is_inside_tree():
return
var tween := get_node("Tween") as Tween
if not is_instance_valid(tween):
return
var old_val := staff_mode
staff_mode = new_val
var travel_direction = sign(new_val - old_val)
for mode in range(old_val, new_val + travel_direction, travel_direction):
match mode:
Modes.full:
tween.interpolate_property($"1", "position", $"1".position, Vector2.ZERO, 0.2)
tween.interpolate_property($"1/2", "position", $"1/2".position, Vector2(0, -35), 0.2)
tween.interpolate_property($"1/2/3", "position", $"1/2/3".position, Vector2(0, -34), 0.2)
Modes.mid:
tween.interpolate_property($"1", "position", $"1".position, Vector2.ZERO, 0.2)
tween.interpolate_property($"1/2", "position", $"1/2".position, Vector2(0, -35), 0.2)
tween.interpolate_property($"1/2/3", "position", $"1/2/3".position, Vector2.ZERO, 0.2)
Modes.small:
tween.interpolate_property($"1", "position", $"1".position, Vector2.ZERO, 0.2)
tween.interpolate_property($"1/2", "position", $"1/2".position, Vector2.ZERO, 0.2)
tween.interpolate_property($"1/2/3", "position", $"1/2/3".position, Vector2.ZERO, 0.2)
tween.start()
yield(tween, "tween_all_completed")
What you can see here is that I have encoded the values from the tracks of from the AnimationPlayer in the source code. Using Tween I get to tell it to interpolate from whatever value the track has to the target position of each state.
I don't know if this performs better or worse compared to AnimationPlayer.
Old Answer
Alright, there are two sides to this problem:
travel is supposed to play the animation, so it is not instantaneous. Thus, if you call stop it will not be able to travel, and you get the error message you got.
Ah, but you cannot call start and travel back to back either. You need to wait the animation to start.
I'll start by not going from one state to the same:
func set_staff_mode(new_val:String) -> void:
if staff_mode == new_val:
return
We are going to need to get the AnimationTree, so we need to ge in the scene tree. Here I use yield so the method returns, and Godot resumes the execute after it gets the "tree_entered" signal:
if not is_inside_tree():
yield(self, "tree_entered")
The drawback of yield, is that it can cause an error if the Node is free before you get the signal. Thus, if you prefer to not use yield, we can do this instead:
if not is_inside_tree():
# warning-ignore:return_value_discarded
connect("tree_entered", self, "set_staff_mode", [new_val], CONNECT_ONESHOT)
return
Here CONNECT_ONESHOT ensures this signal is automatically disconnected. Also, Godot makes sure to disconnect any signals when freeing a Node so this does not have the same issue as yield. However, unlike yield it will not start in the middle of the method, instead it will call the method again.
Alright, we get the AnimationTree:
var animation_tree := get_node("/path/to/AnimationTree")
if not is_instance_valid(animation_tree):
return
And get the AnimationNodeStateMachinePlayback:
var ani_state:AnimationNodeStateMachinePlayback = animation_tree.get("parameters/playback")
Now, if it is not playing, we need to make it playing:
if not ani_state.is_playing():
ani_state.start(new_val)
And now the problem: we need to wait for the animation to start.
In lieu of a better solution, we going to pool for it:
while not ani_state.is_playing():
yield(get_tree(), "idle_frame")
Previously I was suggesting to get the AnimationPlayer so we can wait for "animation_started", but that does not work.
Finally, now that we know it is playing, we can use travel, and update the state:
ani_state.travel(new_val)
staff_mode = new_val
Don't call stop.
You might also want to call property_list_changed_notify() at the end, so Godot reads the new value of staff_mode, which it might not have registered because we didn't change it right away (instead we yielded before changing it). I suppose you could alternatively change the value earlier, before any yield.
By the way, if you want the mid animation to complete in the travel, change the connections going out of mid in the AnimationTree from "Immidiate" to "AtEnd".
Addendum on waiting for travel to end and stop
We can spin wait in a similar fashion as we did to wait for the AnimationNodeStateMachinePlayback to start playing. This time we need to pool two things:
What is the current state of the animation.
What is the playback position on that animation.
As long as the animation is not in the final state and as long as it has not reached the end of that animation, we let one frame pass and check again. Like this:
while (
ani_state.get_current_node() != new_val
or ani_state.get_current_play_position() < ani_state.get_current_length()
):
yield(get_tree(), "idle_frame")
Then you can call stop.
Furthermore, I'll add a check for is_playing. The reason is that this code is waiting for the AnimationTree to complete the state we told it to… But if you call travel again before it finished, it will go to new destination, and thus might never reach the state we expected, which result in spin waiting for ever.
And since it might not have arrived to the state we expected, I decided to query the final state instead of setting staff_mode to new_val. That part of the code now looks like this:
ani_state.travel(new_val)
while (
ani_state.is_playing() and (
ani_state.get_current_node() != new_val
or ani_state.get_current_play_position()
< ani_state.get_current_length()
)
):
yield(get_tree(), "idle_frame")
ani_state.stop()
staff_mode = ani_state.get_current_node()
property_list_changed_notify()

Related

How to change the speed of only a single node?

I have a enemy KinematicBody2D and I want to slow down both it's movement & animation
I tried Engine.time_scale but it slows down the entire game,
so is there a way to slow down only a single node without effecting the others ?
There is no API to do that.
For your KinematicBody2D you could do something like this:
export var speed_factor := 1.0
func _process(delta:float) -> void:
delta *= speed_factor
#...
func _physics_process(delta:float) -> void:
delta *= speed_factor
#...
And for your AnimationPlayer you can set playback_speed (which you can also specify when you call play).
Alternatively you can set playback_process_mode to ANIMATION_PROCESS_MANUAL and then call advance and seek (in _process, for example) to make it advance.

QSlider acting like pushers, how to connect to slot that keeps doing something as long a QSlider.isSliderDown() is True?

I have aQt PySide6 app with a centralWidget taken by a Vispy Canvas (OpenGL).
I try to pilot parameters azimuth and elevation of the camera showing the 3D OpenGL environment shown in central widget of the main window.
I have two QSliders in a DockWidget (range -1.0, 1.0, default value 0.0). I try to connect their isPressed() to a method in which I first move the camera by delta increment based on myslider.value(), then test if myslider.isSliderDown() is True, and if so recursively calls the same method. Else (slider released) it sets back the slider at its origin: my slider.setValue(0).
I so would like to have a kind of joystick behaviour. Wen pressed and dragged, the camera rotates/moves, more or less rapidly following the displacement of the slider, and then stops when slider is released, this one jumping back to center position/value.
All I have with this implementation is an infinite loop as soon as I press and drag the slider. Releasing it does not stop the loop.
Why mySlider.isSliderDown() does not reflect the status of the slider? Am I, because of the recursive call, blocked inside the first slot connection call, never going out to reevaluate GUI state?
If so, how should I code this?
Here is a solution with QTimer:
...
self.cameraModTimer = QTimer(self)
self.cameraModTimer.timeout.connect(self.updateCamera)
self.fovSlider = CameraParamSlider(Qt.Vertical, self, fovLabel, "fov", 1.0e3)
self.fovSlider.sliderPressed.connect(self.startCameraUpdate)
self.fovSlider.sliderReleased.connect(self.stopCameraUpdate)
...
with
#Slot()
def updateCamera(self):
if self.fovSlider.isSliderDown() is True:
self.fovSlider.updateParam()
#Slot()
def startCameraUpdate(self):
"""
Start the QTimer that will trigger updateParam as long as it's not stopped
"""
self.cameraModTimer.start()
#Slot()
def stopCameraUpdate(self):
"""
Stop the QTimer that triggered updateParam. This also sets back the sliders
to their default position and calls a last time updateParam to refresh
sliders labels after having been reset.
"""
self.cameraModTimer.stop()
self.fovSlider.setValue(0.0)
self.fovSlider.updateParam()

What is TileMap:1373

I'm following a tutorial series by Heartbeast.
My grass area2D is being collided with TileMap:1373 in the beginning
extends Node2D
func create_grass_effect():
var GrassEffect = load("res://Effects/GrassEffect.tscn")
var grassEffect = GrassEffect.instance()
var world = get_tree().current_scene
world.add_child(grassEffect)
grassEffect.global_position = global_position
func _on_Hurtbox_area_entered(area):
pass
func _on_Hurtbox_body_shape_entered(body_id, body, body_shape, local_shape):
print(body_id, body, body_shape, local_shape)
func _on_Hurtbox_body_shape_entered(body_id, body, body_shape, local_shape):
print(body_id, body, body_shape, local_shape)
console
1373[TileMap:1373]100
1373[TileMap:1373]100
1373[TileMap:1373]110
Despite moving all tile maps out of the way for the two small green grasses:
When I tell the script to queue_free() on _on_Hurtbox_area_entered:
Flipping the code doesn't work because when I have this code:
extends Node2D
func create_grass_effect():
var GrassEffect = load("res://Effects/GrassEffect.tscn")
var grassEffect = GrassEffect.instance()
var world = get_tree().current_scene
grassEffect.global_position = global_position
world.add_child(grassEffect)
func _on_Hurtbox_area_entered(area):
queue_free()
func _on_Hurtbox_body_shape_entered(body_id, body, body_shape, local_shape):
print(body_id, body, body_shape, local_shape)
The grass disapears anyways.
So the problem must be due to the signal or queue_free()
EDITTTTT:
I was looking and I think this solved my problem:
The question isn't entirely clear on what you want to trigger the signal, but I assume you want it to fire when the player enters the area. The docs for area_entered say:
Emitted when another area enters.
The player is a PhysicsBody, not an Area. Use body_entered instead:
Emitted when a physics body enters.
The body argument can either be a PhysicsBody2D or a TileMap instance (while TileMaps are not physics body themselves, they register their tiles with collision shapes as a virtual physics body).
So I guess the signal is wrong then...
The grass is touching itself and causing it self to queue_free()
I solved the problem by putting the grasses apart from each other.

Continually running functions in tkinter after mainloop in Python 3.4

What I want to do is colour in a single pixel in the centre of the screen, then at random choose an adjacent pixel and colour it in, and then repeat until some condition is met - anything such as time, or the screen is full, or after a certain number of pixels are full. This ending isn't too important, I haven't got that far yet, and I think I could manage to work that out myself.
I have no experience with tkinter, but I decided it was the best way to display this, since I don't really no any other way. Some of this code (mainly the tkinter functions like Canvas, PhotoImage etc) is therefore copy-pasted (and slightly edited) from examples I found here.
What my code does when run is hard to tell - it uses the CPU as much as it can seemingly indefinitely, and slowly increases its memory usage, but doesn't appear to do anything. No window opens, and the IDLE interpreter goes to a blank line, as usual when calculating something. When killed, the window opens, and displays a white page with a little black blob in the bottom right corner - as if the program had done what it was meant to, but without showing it happening, and starting in the wrong place.
So:
Why does it do this?
What should I do to make my program work?
What would be a better way of coding this, changing as many things as you like (ie. no tkinter, a different algorithm etc)?
from tkinter import Tk, Canvas, PhotoImage, mainloop
from random import randrange
from time import sleep
def choose_pixel(pixel_list):
possible_pixels = []
for x in pixel_list:
#adjacent pixels to existing ones
a = [x[0] + 1, x[1]]
b = [x[0] - 1, x[1]]
c = [x[0], x[1] + 1]
d = [x[0], x[1] - 1]
#if a not in pixel_list:
possible_pixels.append(a)
#if b not in pixel_list:
possible_pixels.append(b)
#if c not in pixel_list:
possible_pixels.append(c)
#if d not in pixel_list:
possible_pixels.append(d)
pixel_choosing = randrange(len(possible_pixels))
final_choice = possible_pixels[pixel_choosing]
return final_choice
def update_image(img_name, pixel):
img.put("#000000", (pixel[0], pixel[1]))
WIDTH, HEIGHT = 320, 240
window = Tk()
#create white background image
canvas = Canvas(window, width=WIDTH, height=HEIGHT, bg="#ffffff")
canvas.pack()
img = PhotoImage(width=WIDTH, height=HEIGHT)
canvas.create_image((WIDTH, HEIGHT), image=img, state="normal")
first_pixel = [int(WIDTH/2), int(HEIGHT/2)]
pixel_list = [first_pixel]
img.put("#000000", (first_pixel[0], first_pixel[1]))
canvas.pack()
runs = 0
while True:
next_pixel = choose_pixel(pixel_list)
pixel_list.append(next_pixel)
window.after(0, update_image, img, next_pixel)
canvas.pack()
runs+=1
window.mainloop()
The pattern for running something periodically in tkinter is to write a function that does whatever you want it to do, and then the last thing it does is use after to call itself again in the future. It looks something like this:
import tkinter as tk
...
class Example(...):
def __init__(self, ...):
...
self.canvas = tk.Canvas(...)
self.delay = 100 # 100ms equals ten times a second
...
# draw the first thing
self.draw_something()
def draw_something(self):
<put your code to draw one thing here>
self.canvas.after(self.delay, self.draw_something)
After the function draws something, it schedules itself to run again in the future. The delay defines approximately how long to wait before the next call. The smaller the number, the faster it runs but the more CPU it uses. This works, because between the time after is called and the time elapses, the event loop (mainloop) is free to handle other events such as screen redraws.
While you may think this looks like recursion, it isn't since it's not making a recursive call. It's merely adding a job to a queue that the mainloop periodically checks.

Pygame head-on collisions not detecting

Im really new to Python and recently I've been working on creating a small, space invaders style game in pygame. I've almost reached the end however, I want to make it so if the enemy ships (block) collide with my ship (player), a collision is detected, removing my both ships and displaying a short "Game Over" message.
So far I have the code for detecting when a bullet and an enemy ship collides, I have rewritten this for if my ship and an enemy ship collides but this code only works if I fire no shots, I also have to be moving from side to side for the collision to be detected (head on collisions do nothing) once the collision is detected and both ships disappear I am still able to fire bullets from the position at which the collision was detected. I have no idea why this happens. If anyone could help me out Id definitely appreciate it.
Heres the code in question:
for i in range(15):
block = Block(BLACK)
block.rect.x = random.randrange(screen_width)
block.rect.y = random.randrange(55) # change to 155 collisions fixed
block_list.add(block)
all_sprites_list.add(block)
for i in range(1):
player = Player()
player.rect.y = 480
player_list.add(player)
all_sprites_list.add(player)
...
for player in player_list:
player_hit_list = pygame.sprite.spritecollide(block, player_list, True)
for player in player_hit_list:
gameover.play()
player_list.remove(player)
all_sprites_list.remove(player)
block_list.remove(block)
all_sprites_list.remove(block)
for bullet in bullet_list:
block_hit_list = pygame.sprite.spritecollide(bullet, block_list, True)
for block in block_hit_list:
explosion.play()
bullet_list.remove(bullet)
all_sprites_list.remove(bullet)
score += 10
UPDATE
I have now managed to get the collision to detect properly, however I am still able to fire once the ships have disappeared (due to collision) is there any way I can hide the bullet once the collision has taken place?
Heres my updated code:
for i in range(15):
block = Block(BLACK)
block.rect.x = random.randrange(screen_width)
block.rect.y = random.randrange(55) # change to 155 collisions fixed
block_list.add(block)
all_sprites_list.add(block)
for i in range(1):
player = Player()
player.rect.y = 480
player_list.add(player)
all_sprites_list.add(player)
...
for player in player_list:
block_hit_list = pygame.sprite.spritecollide(player, block_list, True)
for block in block_hit_list:
gameover.play()
player_list.remove(player)
all_sprites_list.remove(player)
block_list.remove(block)
all_sprites_list.remove(block)
for bullet in bullet_list:
block_hit_list = pygame.sprite.spritecollide(bullet, block_list, True)
for block in block_hit_list:
explosion.play()
bullet_list.remove(bullet)
all_sprites_list.remove(bullet)
score += 10
Since you are working with groups, you might want to use this function, for handling collisions between groups:
Would be something like this (I haven't tried your code)
pygame.sprite.groupcollide(bullet_list, block_up_list, True, True, collided = None)
With both arguments True you remove both from the list. When you learn how to use groupcollide, you are going to notice that it's very useful.
Anyways, look for the function description in the pygame documentation and see some examples.
Hope that helps ;)

Resources