Is it possible to call a GdScript function via JavaScript on WASM project export ?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By enzopoeta

Hi guys! im trying to call a funcion that rotates my model every time thats the user clicks on an html button on the page where my godot game canvas is loaded … Is it possible to access the functions stored in the wasm via javascript ? Somebody can give some examples ? Thanks !

I have tried to do something similar but have had no luck. The only form of external communication I could get with an exported Godot game was sending it files via the user:// directory (which in the web export is located in the IndexedDB).

If you were super dedicated to getting something like this working, then you might be able to do some kind of network communication with the Godot game acting as a server to trigger a function call, but that’s a pretty wild hack that probably isn’t even feasible.

literalcitrus | 2018-02-13 22:45

I found two ways of communicating any data between web components and the godot engine:

1. URL parameter polling
You can use the following line of js code in both Godot (as JavScript.eval(…)) and in the actual web environment you are hosting it in to append url parameters:

history.replaceState()

While this is a quick implementation it is also resource heavy since it is based on polling the url parameters every frame.

2. postMessage with callbacks
You use Godots

JavaScript.get_interface()

to register the callback, and JavaScripts

postMessage()

to send data on both ends.

This is a way better solution because it only requires resources when data is sent / received.

It also works its way around CORS Errors insanely well, so you can inject godot into existing websites as sub components (e.g. present your project live on you portfolio homepage).

Since the original question only has 2 upvotes and is from early 2018 I am not gonna bother gathering it all together right now.

Let me know if you need details / code on how to do it.

Ada | 2022-02-03 03:51

Hey hi :slight_smile:

I’m trying to comunicate with my Godot HTML5 export from javascript and while I was trying to figure out how to do it I read your comment and I’d love to receive more details about the second way…

Also, with the JavaScript Godot class to interact with javascript you need to expose the javascript functions as property of window object, but this way users will be able to call the function from the console which I would not want to happen.

Maybe this kind of comunication dosen’t fit my needs? Let me know, thanks :slight_smile:

Sawyer | 2023-03-20 23:27

Hello!
Depending on the development state of your project I would advise against ruling out solutions that work but might have some drawbacks. Often it is better to get something running and optimise it afterwards than to not getting anything to work because of such concerns.

Also, a system is considered safe from being hacked if the cost of hacking it is greater than the gain an attacker would get out of it. Your app can always be target of memory manipulation from things like cheat engine - especially within the web.

So unless you are creating a system that has to be super secure I am sure you won’t be having issues with the JS implementation.


Now to the code. I did not make an MVP project for this but this code should get you going:

var _callback = JavaScript.create_callback(self, "receivedMessage")

func _ready():
	subToMessages()

func subToMessages():
	if OS.has_feature('JavaScript'):
		var window = JavaScript.get_interface("window")
		window.onmessage = _callback

func receivedMessage(args):
	# parse
	var key
	if args[0].message:
		key = 'message'
	else:
		key = 'data'
	var data = args[0][key]
	
	# filter
	if data.find('toGodot:') == 0:
		# message starts with 'toGodot:'
		data = data.replace('toGodot:', '')
		
		# emit or whatever you wanna do with this data
		emit_signal('message', data)

you can test if this works by calling this in JS (make sure to replace the URL):

postMessage(‘toGodot:test’,‘http://127.0.0.1:5500’)

The “toGodot” part of the code is just to filter out any messages that might be sent that are not intended for the Godot application. For an MVP this would not be required but it surely doesn’t harm the integrity aspect.


Edit: On the JS front where you probably have the app integrated via an iframe you call things like

var iframe = document.getElementsByTagName("iframe")[0]
var gdBase = 'toGodot:'

iframe.contentWindow.postMessage(gdBase + 'scroll:' + offset / max * 100, 'https://your.url.com')

2nd Edit: If you want to send data from Godot to JS you can use this code. Now you have a bidirectional instant communication setup:

func postMessage(data, iframe = false, target = "https://your.url.com/"):
	if OS.has_feature('JavaScript'):
		if iframe:
			return JavaScript.eval("""
					window.parent.postMessage('""" + data + """', '""" + target + """')
			""")
		else: 
			# message receiver might change if this features is used in android as well
			return JavaScript.eval("""
					postMessage('""" + data + """', '""" + target + """')
			""")
	return null

Let me know if this works. Could be that i missed some code when copying it all together.

Ada | 2023-03-21 10:36

That sounds really really good, I like it. I will test it right away and let you know, thank you very much.

Just one thing: do you think it’s better to stay on godot 3.5 for these kinds of web projects or could I switch to godot 4?

Sawyer | 2023-03-21 18:17

That is a very good question. All of this code is for 3.x so you could safe yourself some work if you stick with that.

IIRC godot 4 also changed up quite a bit of the JS implementation so you might have to code all that stuff from scratch and with a different approach.

My biggest ick with Godot4 is that last time i tried it it felt very much still wip. I did hit a few roadblocks that just weren’t implemented by the dev team yet. So there were no workarounds or anything like it and stuff just didn’t work as intended. I am sure it is better now but Godot 3.x is definetly more reliable in that aspect since it had time to age (and it won’t be phased out anytime soon either).

That being said, Godot 4 is really good and especially the performance gain might be interesting depending on your project. However there aren’t a lot of resources out there for Godot 4 so you kinda already have to know what you are doing with it since all the tutorials are mostly for 3.x.

tl;dr: I would go with Godot 3 since it is more stable, reliable and there are more resources available for it. You can always add some minor tweaks to migrate to Godot 4 later on. Also I can guarantee you that the web aspect of Godot 3 is just a few clicks and works amazingly well without any hitches - which I can not guarantee you for Godot 4.

Ada | 2023-03-21 19:02

Great, I was more or less of the same idea, but you have definitely solved all my doubts. I was excited to get my hands on Godot 4, but I will definitely stick on the 3.5.x, thank you.

About the code: I tested it, it works (except for one little thing I’ll show you now) and I love it, it feels like a simplified version of a web socket connection.

Anyway, at first glance it was working but then it started to clash with other messages. In fact, when I refreshed the page, and not only mine but others as well, a message from the metamask extension was caught in Godot. Evidently, the extension sends a post message to all active tabs when a new page is opened or refreshed.

But it was just a problem of not filtering the message correctly, so I easily solved it this way:

I made the message from JS more specific

iFrameRef.current.contentWindow.postMessage({"GODOT": true, "ACTION": "some action"}, "http://localhost:3000")

While in godot I modified the callback so that it only captures the affected message:

func receivedMessage(args):
  if !args[0].data.GODOT: return 
  var action = args[0].data.ACTION
  printt("MESSAGE FOR GODOT: ", action)
  emit_signal("message_received", action)

And it’s all perfect! I didn’t try the postMessage from Godot yet but I think the process is the same, just change the message so that it can be recognizable and filtered by JavaScript.

To close I have just a one more question about your implementation of the receivedMessage function in Godot: can the message key be “message” instead of “data”? I seemed to see, even from other messages, that it is always data. If yes, on what occasions can the key be “message”?

Sawyer | 2023-03-21 21:35

Yeah I experienced the same issue where a lot of messages flooded the input. Thats why I have the # filter part in my receivedMessage function.

data.find('toGodot:') == 0

iirc this checks if “toGodot:” is in the message string at the 0th index - so at the start. Yours does pretty much the same so that is definetly fine - might even be cleaner tho I don’t know if object passing like that works for every case.

I don’t remember why I filtered for “message” and “data”. Maybe “message” is set when it is only a string as payload, otherwise the data field is populated? I also had browser plugins that sent lots of those postMessages so maybe one or more of them interacted like that.

I don’t think it is essential but you could surely read the offical postMessage documentation to find out when which case is active.

Good luck on your project and hit me up if you need anything!

Ada | 2023-03-21 22:39

Yes but, since the metamask message that was giving me problems was not a string instead of being filtered it gave me error when trying to call the .find(). Oh and the data of the message was an object, from there I got the idea to make my message an object too.

Thanks a lot! I’m very lucky that you responded to me even after 1 year, you definetly saved me.

Good luck to you too

Sawyer | 2023-03-21 23:05

:bust_in_silhouette: Reply From: dsina

I’ve tried to do the same. This might not be quite what you asked for and it’s a bit hacky but it might work for you. I am also interested in other solutions btw. Basically Javascript.eval() can return certain datatypes(check docs for full list). Here’s how I use this. I set a certain js variable for example when a html button is pressed and then in godot poll that variable during the process step. I then convert what was stored in that variable to a godot InputAction.

This is the part in my gdscripts that sets up the js environment with the necessary things:

JavaScript.eval("""
	function Input(action){
		this.action=action
		this.processed=false
		this.data=""
	}
	
	function Input(action, processed, data){
		this.action=action
		this.processed=processed
		this.data=data
	}

	//starting input, doesn't need to be processed, so processed=true
	var input=new Input("default")
	input.processed=true
	
	function setInput(newInput){
		input=newInput
	}
	var godotInputActions=new Object()
""",true)

# creates a js object containing strings for all godot actions
# dont try to create inputs with other actions in js
for action in InputMap.get_actions():
	JavaScript.eval("""
		godotInputActions.%s="%s"
	""" % [action,action],true)

Here’s the js code for what my html button does(had to add that to godot.html from the webassembly export templates):

document.getElementById('output-button').addEventListener('click', () => {
				setInput(new Input(godotInputActions.ui_button))
			});

This is the function that grabs new input(called during each process step):

func _fetch_js_input():
    var input_action=JavaScript.eval("""input.action""")
    var input_processed=JavaScript.eval("""input.processed""")
    var input_data=JavaScript.eval("""input.data""")

    return {"action": input_action, "processed": input_processed, "data": input_data}

This marks the js variable as processed(called e.g. after _fetch_js_input):

func _set_js_input_as_processed():
    JavaScript.eval("""input.processed=true""",true)

Then I can do whatever is needed where these inputActions are processed:

func _input(event):
    if (event is InputEventAction and event.action=="ui_button"):
        print("Button pressed");

Thanks ! Works like a charm in my project !

enzopoeta | 2018-02-15 12:17

Hello! I attempted to repeat the above process you created, but I’m having a small issue. Every time I use the triple quotation marks in my JavaScript.eval() function it’s commenting out that content. My JS is multi-line strings, so single quotations won’t work (I get an unexpected EOL error). Is there some way around this or am I missing something? Any help would be greatly appreciated!

BxKat | 2018-12-03 06:48