touilleMan / godot-python

Python support for Godot 🐍🐍🐍
Other
1.88k stars 143 forks source link

Need some clarity on signals #199

Open shayneoneill opened 4 years ago

shayneoneill commented 4 years ago

Hi, I'm trying to work out how the signals work in this, I cant really find it documented.

I'm trying to create a listener that sits on a root node , that can accept "towns" and "ids"

so something like register_town.= signal(id,town)

That of course doesn't work (For a start its unpythonic, whats an id , whats a town), but I cant really see any other way to specify those paramaters.

Secondly, even if I do just register_town = signal(), I never see that signal turn up in the inspector. Should it?

adracea commented 4 years ago

@shayneoneill hey , I've been toying around with using this project as well . What I've noticed :

  1. You declare signals indeed like register_town = signal() . In order for them to pop up you have to restart the godot editor/reload project .
  2. Currently , the only way I've found of "emitting signals" is using self.call("emit_signal","register_town")

I'm currently still struggling with finding a way to wait for signals or finding any documentation on doing it .( using the yield method that godot has)

shayneoneill commented 4 years ago

Ah ok. Thats troublesome. I'm now kind of wondering if I should perhaps just roll my own signal method. Though the usual call-back instant dispatch wouldn't work as its crossing too many barriers. Kinda feels like fighting against the system, which is never a good thing in programming.

cr8ivecodesmith commented 4 years ago

I'm currently still struggling with finding a way to wait for signals or finding any documentation on doing it .( using the yield method that godot has)

Were you able to figure out a way to do this? I'm currently trying out the "Your first game" tutorial and that uses the yield call of GDScript. I had to settle with boolean flags for now. What's a good workaround for this in the mean time?

ghost commented 4 years ago

This is literally the only documentation I can find on how one does signals in godot-python, but, how does one do signals that pass parameters? Do you just add arguments to the end of the self.call("emit_signal", "signal_name", ... )?

omardelarosa commented 3 years ago

This thread is unfortunately the only docs I have found. However, through some experimentation and looking at the source code for more information on how Signals are implemented, they definitely support argument passing and here's how it seems to work (at least as of my current godot version of 3.2.4beta):

# something.py

from godot import *

class Something(Node)
    # this variable name must match the string below
    player_move_success = signal()

   def player_has_moved_to(self, vec3_from_position, vec3_to_position):
        # After ensuring that both vec3 args are instances of Vector3() class...
        self.call("emit_signal", "player_move_success", vec3_from_position, vec3_to_position)

Then from the GDScript side:

extends Node

# Assuming there is a Something node as a child in the scene with the Python something.py attached to it.
onready var something_node = $Something

func _ready():
     # Observe the Python-based Something node's "player_move_success" signal
     something_node.connect("player_move_success", self, "_on_player_move_success")

func _on_player_move_success(from_position: Vector3, to_position: Vector3):
     print("player has successfully moved from ", from_position, " to ", to_position)

That being said keep in mind that the function signature's arity (i.e. the number of arguments) must match between the Python call and the observer. For example, these will not work and silently fail:

# Sending a mismatched function with only 1 argument
self.call("emit_signal", "player_move_success", vec3_from_position)

# Sending a mismatched function with no arguments
self.call("emit_signal", "player_move_success")
MarioMey commented 2 years ago

Anyone: this is still the best way to "emit signals"? Is this the best way to comunicate between code files? I used to use bge.logic.sendMessage(subject, body) a lot to comunicate between objects in Blender Game Engine. I think signals are the alternative in Godot, aren't they? Is there something better?

@adracea Did you find the way to wait for signals?

@cr8ivecodesmith Could you share the "Your first game" converted to Python? I'm doing the same right now, it is almost complete.

@shayneoneill Could you share your "own signal method"?

@cridenour Thanks for bring me here.

MarioMey commented 2 years ago

Well, after learning a bit about yield, I think I found an alternative in Python. Also with yield, but a bit more complicated. This is HUD.py, the GDScritp to Python converted code of "Your first game" (I only add comments where it is relevant to this thread):

from godot import exposed, export, signal, CanvasLayer
import time

@exposed
class HUD(CanvasLayer):
    start_game = signal()

    def _ready(self):
        self.connect("start_game", self.get_node("/root/Main"), "new_game")
        self.get_node("StartButton").connect("pressed", self, "_on_StartButton_pressed")
        self.get_node("MessageTimer").connect("timeout", self, "_on_MessageTimer_timeout")

    def show_message(self, text):
        self.get_node("Message").text = text
        self.get_node("Message").show()
        self.get_node("MessageTimer").start()

    # New function name
    def show_game_over_yield(self):
        self.show_message("Game Over")
                # Needed for the generator creation
        while True:
            # Wait for 'MessageTimer_timeout' message
            if (yield) == 'MessageTimer_timeout':
                self.get_node("Message").text = "Dodge the\nCreeps!"
                self.get_node("Message").show()
                time.sleep(1)
                self.get_node("StartButton").show()
                # We need to delete this generator now
                del self.sgo

        # This is the main function that is executed by Main.py
    def show_game_over(self):
                # Create the yield generator and run it till the yield line.
        self.sgo = self.show_game_over_yield()
        next(self.sgo)

    def update_score(self, score):
        self.get_node("ScoreLabel").text = str(score)

    def _on_StartButton_pressed(self):
        self.get_node("StartButton").hide()
        self.call("emit_signal", "start_game")

    def _on_MessageTimer_timeout(self):
        self.get_node("Message").hide()
                # Check if the generator is created and send message to it
        if hasattr(self, 'sgo'):
            self.sgo.send('MessageTimer_timeout')
MarioMey commented 2 years ago

Here are the other codes. I added lines to connect signals by code, instead of doing at hand. I always tried to do it in this way, when working with Blender Game Engine.

Main.py from godot import exposed, export, signal, Vector2, Node, PackedScene, ResourceLoader import random, math @exposed class Main(Node): mob_scene = ResourceLoader.load("res://Mob.tscn") score = int() def _ready(self): self.get_node("MobTimer").connect("timeout", self, "_on_MobTimer_timeout") self.get_node("ScoreTimer").connect("timeout", self, "_on_ScoreTimer_timeout") self.get_node("StartTimer").connect("timeout", self, "_on_StartTimer_timeout") pass def game_over(self): self.get_node("ScoreTimer").stop() self.get_node("MobTimer").stop() self.get_node("HUD").show_game_over() self.get_node("Music").stop() self.get_node("DeathSound").play() def new_game(self, mensaje=None): print(mensaje) self.score = 0 self.get_node("Player").start(self.get_node("StartPosition").position) self.get_node("StartTimer").start() self.get_node("HUD").update_score(self.score) self.get_node("HUD").show_message("Get Ready") self.get_tree().call_group("mobs", "queue_free") self.get_node("Music").play() def _on_ScoreTimer_timeout(self): self.score += 1 self.get_node("HUD").update_score(self.score) def _on_StartTimer_timeout(self): self.get_node("MobTimer").start() self.get_node("ScoreTimer").start() def _on_MobTimer_timeout(self): mob = self.mob_scene.instance() mob_spawn_location = self.get_node("MobPath/MobSpawnLocation") mob_spawn_location.offset = random.randrange(pow(2,32)) direction = mob_spawn_location.rotation + math.pi / 2 mob.position = mob_spawn_location.position direction += random.uniform(-math.pi / 4, math.pi / 4) mob.rotation = direction velocity = Vector2(random.uniform(150.0, 250.0), 0.0) mob.linear_velocity = velocity.rotated(direction) self.add_child(mob)
Player.py from godot import exposed, export, signal, Vector2, Area2D, InputEventKey from godot import KEY_RIGHT, KEY_LEFT, KEY_UP, KEY_DOWN @exposed class Player(Area2D): hit = signal() speed = export(int, default=400) screen_size = int() velocity = Vector2.ZERO velocity_real = Vector2.ZERO def _ready(self): self.connect("hit", self.get_node("/root/Main"), "game_over") self.connect("body_entered", self, "_on_Player_body_entered") screen_size = self.get_viewport_rect().size self.hide() def _unhandled_input(self, event): if type(event) is InputEventKey: if event.pressed and event.scancode == KEY_RIGHT and not event.echo: self.velocity.x += 1 if event.pressed and event.scancode == KEY_LEFT and not event.echo: self.velocity.x -= 1 if not event.pressed and event.scancode in [KEY_RIGHT, KEY_LEFT]: self.velocity.x = 0 if event.pressed and event.scancode == KEY_DOWN and not event.echo: self.velocity.y += 1 if event.pressed and event.scancode == KEY_UP and not event.echo: self.velocity.y -= 1 if not event.pressed and event.scancode in [KEY_UP, KEY_DOWN]: self.velocity.y = 0 if self.velocity.length() > 0: self.velocity_real = self.velocity.normalized() * self.speed self.get_node("AnimatedSprite").play() else: self.velocity_real = self.velocity self.get_node("AnimatedSprite").stop() def _process(self, delta): self.position += self.velocity_real * delta self.screen_size = self.get_viewport_rect().size self.position.x = max(0, min(self.position.x, self.screen_size.x)) self.position.y = max(0, min(self.position.y, self.screen_size.y)) if self.velocity.x != 0: self.get_node("AnimatedSprite").animation = "walk" self.get_node("AnimatedSprite").flip_v = False self.get_node("AnimatedSprite").flip_h = self.velocity.x < 0 elif self.velocity.y != 0: self.get_node("AnimatedSprite").animation = "up" self.get_node("AnimatedSprite").flip_v = self.velocity.y > 0 def _on_Player_body_entered(self, _body): self.hide() self.call("emit_signal", "hit") self.get_node("CollisionShape2D").set_deferred("disabled", True) def start(self, pos): self.position = pos self.show() self.get_node("CollisionShape2D").disabled = False
Mob.py from godot import exposed, RigidBody2D import random @exposed class Mob(RigidBody2D): def _ready(self): self.get_node("VisibilityNotifier2D").connect("screen_exited", self, "_on_VisibilityNotifier2D_screen_exited") self.get_node("AnimatedSprite").playing = True mob_types = self.get_node("AnimatedSprite").frames.get_animation_names() anim = mob_types[random.randint(0, len(mob_types) - 1)] self.get_node("AnimatedSprite").animation = anim def _on_VisibilityNotifier2D_screen_exited(self): self.queue_free()