Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Object-level arbitrary message sending #11330

Open
BajaTheFrog opened this issue Dec 10, 2024 · 6 comments
Open

Add Object-level arbitrary message sending #11330

BajaTheFrog opened this issue Dec 10, 2024 · 6 comments

Comments

@BajaTheFrog
Copy link

Describe the project you are working on

I mostly work on 2D games but every project I have worked on involves some sort of collision or area detection where information needs to ultimately needs to be shared about the nature of the intersection.

This is what has led me to think about this proposal but it has lots of potential applications.

Describe the problem or limitation you are having in your project

What I would like is to have the option for very open-ended "messaging" to be possible with anything that inherits from Object.

Right now, if I want to pass information from one colliding body to another I essentially have to use some combination of groups and/or casting before I can confidently interact with a colliding object.

Example: A bullet colliding with a player and applying damage

# Bullet.gd

# this is pretty brittle in case this group changes or the method convention changes
# not to mention if the body needs to exercise a more complicated handling of a bullet based on stats or whatever
# so this is the most compact version that won't risk throwing an error 

func _on_body_entered(body) -> void:
     if body.is_in_group("damageable") and body.has_method("take_damage"):
        body.take_damage(10) 

This is fine and sometimes preferred, but many times I just want to tell an object (without knowing what it is) that it collided with a particular type of object.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

I would like a feature where any Object can accept some arbitrary information and then the script that receives this information can decide to do something with it (or not).

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

I am imagining something like this

# Object.gd

# id could be an int or string, don't have super strong feelings at the moment
func message(id: string, payload: Dictionary) -> void:
    pass

And then classes can override this to suit their needs

# Bullet.gd

const MSG_HIT = "bullet hit"
const DAMAGE_KEY = "bullet damage"

static func get_damage(payload: Dictionary) -> int:
    return payload[DAMAGE_KEY]

func _on_body_entered(body) -> void:
      body.message(MSG_HIT, { DAMAGE_KEY : 10 })
# Player.gd

# Compared to our previous example, we don't need to add to any groups or implement some 
# specific list of functions that the bullet needs to check against. The message receiver decides what to listen for. 

func message(id: string, payload: Dictionary) -> void:
    if id == Bullet.MSG_HIT:
        health -= Bullet.get_damage(payload)

Maybe we would want message to automatically propagate up to parents as well but thats a bigger discussion.

If this enhancement will not be used often, can it be worked around with a few lines of script?

You can work around it by making your own subclasses that have this functionality - which doesn't scale well if you want to use it in a lot of places. You can also kind of abuse _notification to get a simple message but

  1. It won't include the payload
  2. It risks colliding with system message values if you aren't careful about the values you pick for your own messages

So there are a lot of ways to solve the actual functionality but none as easy or as universal.

Is there a reason why this should be core and not an add-on in the asset library?

I think this is a simple and non-intrusive addition that needs to sit at the Object level to be useful, otherwise you need to subclass everything explicitly to get this behavior.

@Meorge
Copy link

Meorge commented Dec 10, 2024

Soon, traits may be able to help with this: godotengine/godot#97657

# messageable.gdt
trait_name Messageable

func message(id: String, payload: Dictionary) -> void
# bullet.gd
class_name Bullet
extends Node2D

static func get_damage(payload: Dictionary) -> int:
    return payload[DAMAGE_KEY]

func _on_body_entered(body) -> void:
    if body is Messageable:
        body.message(MSG_HIT, { DAMAGE_KEY : 10 })
# player.gd
class_name Player
extends Node2D
uses Messageable

func message(id: String, payload: Dictionary) -> void:
    if id == Bullet.MSG_HIT:
        health -= Bullet.get_damage(payload)

This way, you'll be able to just add uses Messageable to the top of the class and implement your message method.

Note that this is not merged yet, but it seems like it might be getting close!

@BajaTheFrog
Copy link
Author

Soon, traits may be able to help with this: godotengine/godot#97657

True! That will be cool when that drops - though I still think there is value in this as a base Object behavior.

@KoBeWi
Copy link
Member

KoBeWi commented Dec 11, 2024

 if body.is_in_group("damageable") and body.has_method("take_damage"):
   body.take_damage(10) 

The group check is redundant. You can just assume that if object has take_damage method, it's damageable. Then add a wrapper for safe calls like

func send_message(object, method, data):
	if object.has_method(method):
		object.call(method, data)

and your code becomes

func _on_body_entered(body) -> void:
	Globals.send_message(body, "take_damage", 10)

@BajaTheFrog
Copy link
Author

BajaTheFrog commented Dec 11, 2024

Sure - its maybe not the most interesting example. You could easily imagine a slightly different group relationship that would require such a check like:

 if body.is_in_group("bullet_vulnerable") and body.has_method("take_damage"):
     body.take_damage(10) 

And with your global wrapper

 if body.is_in_group("bullet_vulnerable"):
     Globals.send_message(body, "take_damage", 10)

But now I would have to make sure everything that can take damage from a bullet has a take_damage function with the same signature.

Example specifics aside - I think the primary benefit of a message function on Object allows individual objects to decide how they are affected by a message (if at all) rather than the messenger figuring out if they meet specific criteria or not (i.e. checking group, has_method, or casting).

Same reason why _notification is designed the way it is, you get some notification from the system and decide which ones you care about responding to or not, rather than having to implement specific functions that map to particular notifications.

@kleonc
Copy link
Member

kleonc commented Dec 11, 2024

@BajaTheFrog But you can do e.g.:

class_name Globals

static func message(receiver: Object, id: string, payload: Dictionary) -> void:
	assert(is_instance_valid(receiver))
	if receiver.has_method(message):
		receiver.message(id, payload)

Then in your proposed Bullet.gd change:

func _on_body_entered(body) -> void:      
	  Globals.message(body, MSG_HIT, { DAMAGE_KEY : 10 }) #body.message(MSG_HIT, { DAMAGE_KEY : 10 })

And you already have the behavior you wanted, no changes to Player.gd needed.

Of course it would fail if message function of the given receiver has different signature than message(id: string, payload: Dictionary). But it would fail as well for the proposed/requested built-in virtual/overridable message function. The difference is the above would fail at runtime, while for the virtual function the signature mismatch could be detected within the editor by the GDScript analyzer.

Matching signature could be guaranteed in the future by using already mentioned traits (which are planned to be added):

class_name Globals

static func message(receiver: Object, id: string, payload: Dictionary) -> void:
	assert(is_instance_valid(receiver))
	if receiver is Messageable:
		receiver.message(id, payload)
	#elif receiver.has_method("message"):
		# Possibly was meant to be Messageable, warn?
# Player.gd
...
uses Messageable
...

@BajaTheFrog
Copy link
Author

Yep! that's all true and combined with traits it works.
Given the choice between the two, would you consider the above preferred to having message available on Object?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants